Compare commits
74 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5b0903d162 | ||
|
10669be7c0 | ||
|
2edae741e1 | ||
|
ea90e553d1 | ||
|
b0cfd86902 | ||
|
565a9540c9 | ||
|
fd10e27b99 | ||
|
39763d772c | ||
|
ee69b77bfb | ||
|
353444e56d | ||
|
112ba1e31f | ||
|
9c3cd9e69c | ||
|
685d8317bc | ||
|
f42d102b26 | ||
|
c944e9ef94 | ||
|
f10d6e8bef | ||
|
f3f3af79fd | ||
|
0e63aa0f6b | ||
|
b254e17e89 | ||
|
5c059b66f0 | ||
|
c628bef666 | ||
|
2270db3f7a | ||
|
d0d467813d | ||
|
123f111efe | ||
|
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 | ||
|
44fdf95d99 | ||
|
0a62614eec | ||
|
97758b343b | ||
|
fb6f52b360 | ||
|
786cd2a539 | ||
|
45793f1254 | ||
|
b621756d92 | ||
|
a5b5c700a6 | ||
|
ea95920f31 | ||
|
d7602f3c08 | ||
|
a20addd05e | ||
|
d9d89a6fa0 | ||
|
3c26376326 | ||
|
d3294bcb86 | ||
|
dda314b518 | ||
|
055a3c339a | ||
|
51f37100a1 | ||
|
88e8265cd6 | ||
|
6e2db882e6 | ||
|
3e2823df1b | ||
|
d02aa18839 | ||
|
b6062f0644 | ||
|
c5fada6e69 |
16
.travis.yml
16
.travis.yml
@@ -1,11 +1,17 @@
|
|||||||
language: go
|
language: go
|
||||||
go:
|
sudo: false
|
||||||
- 1.3
|
matrix:
|
||||||
- 1.2
|
include:
|
||||||
|
- go: 1.4
|
||||||
|
env: TOOLS_CMD=golang.org/x/tools/cmd
|
||||||
|
- go: 1.3
|
||||||
|
env: TOOLS_CMD=code.google.com/p/go.tools/cmd
|
||||||
|
- go: 1.2
|
||||||
|
env: TOOLS_CMD=code.google.com/p/go.tools/cmd
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- go get code.google.com/p/go.tools/cmd/cover
|
- go get ${TOOLS_CMD}/cover
|
||||||
- go get code.google.com/p/go.tools/cmd/vet
|
- go get ${TOOLS_CMD}/vet
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- ./test
|
- ./test
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
# Using Cloud-Config
|
# Using Cloud-Config
|
||||||
|
|
||||||
CoreOS allows you to declaratively customize various OS-level items, such as network configuration, user accounts, and systemd units. This document describes the full list of items we can configure. The `coreos-cloudinit` program uses these files as it configures the OS after startup or during runtime. Your cloud-config is processed during each boot.
|
CoreOS allows you to declaratively customize various OS-level items, such as network configuration, user accounts, and systemd units. This document describes the full list of items we can configure. The `coreos-cloudinit` program uses these files as it configures the OS after startup or during runtime.
|
||||||
|
|
||||||
|
Your cloud-config is processed during each boot. Invalid cloud-config won't be processed but will be logged in the journal. You can validate your cloud-config with the [CoreOS validator]({{site.url}}/validate) or by running `coreos-cloudinit -validate`.
|
||||||
|
|
||||||
## Configuration File
|
## Configuration File
|
||||||
|
|
||||||
@@ -16,7 +18,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.
|
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`
|
- `coreos`
|
||||||
- `ssh_authorized_keys`
|
- `ssh_authorized_keys`
|
||||||
@@ -46,13 +48,13 @@ If the platform environment supports the templating feature of coreos-cloudinit
|
|||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
coreos:
|
coreos:
|
||||||
etcd:
|
etcd:
|
||||||
name: node001
|
name: node001
|
||||||
# generate a new token for each unique cluster from https://discovery.etcd.io/new
|
# generate a new token for each unique cluster from https://discovery.etcd.io/new
|
||||||
discovery: https://discovery.etcd.io/<token>
|
discovery: https://discovery.etcd.io/<token>
|
||||||
# multi-region and multi-cloud deployments need to use $public_ipv4
|
# multi-region and multi-cloud deployments need to use $public_ipv4
|
||||||
addr: $public_ipv4:4001
|
addr: $public_ipv4:4001
|
||||||
peer-addr: $private_ipv4:7001
|
peer-addr: $private_ipv4:7001
|
||||||
```
|
```
|
||||||
|
|
||||||
...will generate a systemd unit drop-in like this:
|
...will generate a systemd unit drop-in like this:
|
||||||
@@ -66,7 +68,6 @@ Environment="ETCD_PEER_ADDR=192.0.2.13:7001"
|
|||||||
```
|
```
|
||||||
|
|
||||||
For more information about the available configuration parameters, see the [etcd documentation][etcd-config].
|
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._
|
_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 +81,9 @@ The `coreos.fleet.*` parameters work very similarly to `coreos.etcd.*`, and allo
|
|||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
coreos:
|
coreos:
|
||||||
fleet:
|
fleet:
|
||||||
public-ip: $public_ipv4
|
public-ip: $public_ipv4
|
||||||
metadata: region=us-west
|
metadata: region=us-west
|
||||||
```
|
```
|
||||||
|
|
||||||
...will generate a systemd unit drop-in like this:
|
...will generate a systemd unit drop-in like this:
|
||||||
@@ -97,6 +98,55 @@ For more information on fleet configuration, see the [fleet documentation][fleet
|
|||||||
|
|
||||||
[fleet-config]: https://github.com/coreos/fleet/blob/master/Documentation/deployment-and-configuration.md#configuration
|
[fleet-config]: https://github.com/coreos/fleet/blob/master/Documentation/deployment-and-configuration.md#configuration
|
||||||
|
|
||||||
|
#### flannel
|
||||||
|
|
||||||
|
The `coreos.flannel.*` parameters also work very similarly to `coreos.etcd.*`
|
||||||
|
and `coreos.fleet.*`. They can be used to set environment variables for
|
||||||
|
flanneld. For example, the following cloud-config...
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
#cloud-config
|
||||||
|
|
||||||
|
coreos:
|
||||||
|
flannel:
|
||||||
|
etcd-prefix: /coreos.com/network2
|
||||||
|
```
|
||||||
|
|
||||||
|
...will generate a systemd unit drop-in like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Service]
|
||||||
|
Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network2"
|
||||||
|
```
|
||||||
|
|
||||||
|
For the complete list of flannel configuraion parameters, see the [flannel documentation][flannel-readme].
|
||||||
|
|
||||||
|
[flannel-readme]: https://github.com/coreos/flannel/blob/master/README.md
|
||||||
|
|
||||||
|
#### locksmith
|
||||||
|
|
||||||
|
The `coreos.locksmith.*` parameters can be used to set environment variables
|
||||||
|
for locksmith. For example, the following cloud-config...
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
#cloud-config
|
||||||
|
|
||||||
|
coreos:
|
||||||
|
locksmith:
|
||||||
|
endpoint: example.com:4001
|
||||||
|
```
|
||||||
|
|
||||||
|
...will generate a systemd unit drop-in like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Service]
|
||||||
|
Environment="LOCKSMITHD_ENDPOINT=example.com:4001"
|
||||||
|
```
|
||||||
|
|
||||||
|
For the complete list of locksmith configuraion parameters, see the [locksmith documentation][locksmith-readme].
|
||||||
|
|
||||||
|
[locksmith-readme]: https://github.com/coreos/locksmith/blob/master/README.md
|
||||||
|
|
||||||
#### update
|
#### update
|
||||||
|
|
||||||
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
|
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
|
||||||
@@ -135,6 +185,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.
|
- **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.
|
- **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.
|
- **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.
|
**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 +200,34 @@ Write a unit to disk, automatically starting it.
|
|||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
coreos:
|
coreos:
|
||||||
units:
|
units:
|
||||||
- name: docker-redis.service
|
- name: docker-redis.service
|
||||||
command: start
|
command: start
|
||||||
content: |
|
content: |
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Redis container
|
Description=Redis container
|
||||||
Author=Me
|
Author=Me
|
||||||
After=docker.service
|
After=docker.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Restart=always
|
Restart=always
|
||||||
ExecStart=/usr/bin/docker start -a redis_server
|
ExecStart=/usr/bin/docker start -a redis_server
|
||||||
ExecStop=/usr/bin/docker stop -t 2 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:
|
Start the built-in `etcd` and `fleet` services:
|
||||||
@@ -167,11 +236,11 @@ Start the built-in `etcd` and `fleet` services:
|
|||||||
#cloud-config
|
#cloud-config
|
||||||
|
|
||||||
coreos:
|
coreos:
|
||||||
units:
|
units:
|
||||||
- name: etcd.service
|
- name: etcd.service
|
||||||
command: start
|
command: start
|
||||||
- name: fleet.service
|
- name: fleet.service
|
||||||
command: start
|
command: start
|
||||||
```
|
```
|
||||||
|
|
||||||
### ssh_authorized_keys
|
### ssh_authorized_keys
|
||||||
@@ -303,11 +372,13 @@ Each item in the list may have the following keys:
|
|||||||
|
|
||||||
- **path**: Absolute location on disk where contents should be written
|
- **path**: Absolute location on disk where contents should be written
|
||||||
- **content**: Data to write at the provided `path`
|
- **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>`.
|
- **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>`.
|
||||||
|
- **encoding**: Optional. The encoding of the data in content. If not specified this defaults to the yaml document encoding (usually utf-8). Supported encoding types are:
|
||||||
|
- **b64, base64**: Base64 encoded content
|
||||||
|
- **gz, gzip**: gzip encoded content, for use with the !!binary tag
|
||||||
|
- **gz+b64, gz+base64, gzip+b64, gzip+base64**: Base64 encoded gzip content
|
||||||
|
|
||||||
Explicitly not implemented is the **encoding** attribute.
|
|
||||||
The **content** field must represent exactly what should be written to disk.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
#cloud-config
|
#cloud-config
|
||||||
@@ -322,6 +393,24 @@ write_files:
|
|||||||
owner: root
|
owner: root
|
||||||
content: |
|
content: |
|
||||||
Good news, everyone!
|
Good news, everyone!
|
||||||
|
- path: /tmp/like_this
|
||||||
|
permissions: 0644
|
||||||
|
owner: root
|
||||||
|
encoding: gzip
|
||||||
|
content: !!binary |
|
||||||
|
H4sIAKgdh1QAAwtITM5WyK1USMqvUCjPLMlQSMssS1VIya9KzVPIySwszS9SyCpNLwYARQFQ5CcAAAA=
|
||||||
|
- path: /tmp/or_like_this
|
||||||
|
permissions: 0644
|
||||||
|
owner: root
|
||||||
|
encoding: gzip+base64
|
||||||
|
content: |
|
||||||
|
H4sIAKgdh1QAAwtITM5WyK1USMqvUCjPLMlQSMssS1VIya9KzVPIySwszS9SyCpNLwYARQFQ5CcAAAA=
|
||||||
|
- path: /tmp/todolist
|
||||||
|
permissions: 0644
|
||||||
|
owner: root
|
||||||
|
encoding: base64
|
||||||
|
content: |
|
||||||
|
UGFjayBteSBib3ggd2l0aCBmaXZlIGRvemVuIGxpcXVvciBqdWdz
|
||||||
```
|
```
|
||||||
|
|
||||||
### manage_etc_hosts
|
### manage_etc_hosts
|
||||||
|
2
Godeps/Godeps.json
generated
2
Godeps/Godeps.json
generated
@@ -28,7 +28,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "gopkg.in/yaml.v1",
|
"ImportPath": "gopkg.in/yaml.v1",
|
||||||
"Rev": "feb4ca79644e8e7e39c06095246ee54b1282c118"
|
"Rev": "9f9df34309c04878acc86042b16630b0f696e1de"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
3
Godeps/_workspace/src/gopkg.in/yaml.v1/LICENSE
generated
vendored
3
Godeps/_workspace/src/gopkg.in/yaml.v1/LICENSE
generated
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
|
||||||
|
Copyright (c) 2011-2014 - Canonical Inc.
|
||||||
|
|
||||||
This software is licensed under the LGPLv3, included below.
|
This software is licensed under the LGPLv3, included below.
|
||||||
|
|
||||||
As a special exception to the GNU Lesser General Public License version 3
|
As a special exception to the GNU Lesser General Public License version 3
|
||||||
|
8
Godeps/_workspace/src/gopkg.in/yaml.v1/README.md
generated
vendored
8
Godeps/_workspace/src/gopkg.in/yaml.v1/README.md
generated
vendored
@@ -12,10 +12,10 @@ C library to parse and generate YAML data quickly and reliably.
|
|||||||
Compatibility
|
Compatibility
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
The yaml package is almost compatible with YAML 1.1, including support for
|
The yaml package supports most of YAML 1.1 and 1.2, including support for
|
||||||
anchors, tags, etc. There are still a few missing bits, such as document
|
anchors, tags, map merging, etc. Multi-document unmarshalling is not yet
|
||||||
merging, base-60 floats (huh?), and multi-document unmarshalling. These
|
implemented, and base-60 floats from YAML 1.1 are purposefully not
|
||||||
features are not hard to add, and will be introduced as necessary.
|
supported since they're a poor design and are gone in YAML 1.2.
|
||||||
|
|
||||||
Installation and usage
|
Installation and usage
|
||||||
----------------------
|
----------------------
|
||||||
|
92
Godeps/_workspace/src/gopkg.in/yaml.v1/decode.go
generated
vendored
92
Godeps/_workspace/src/gopkg.in/yaml.v1/decode.go
generated
vendored
@@ -1,6 +1,8 @@
|
|||||||
package yaml
|
package yaml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -63,7 +65,7 @@ func (p *parser) destroy() {
|
|||||||
func (p *parser) skip() {
|
func (p *parser) skip() {
|
||||||
if p.event.typ != yaml_NO_EVENT {
|
if p.event.typ != yaml_NO_EVENT {
|
||||||
if p.event.typ == yaml_STREAM_END_EVENT {
|
if p.event.typ == yaml_STREAM_END_EVENT {
|
||||||
panic("Attempted to go past the end of stream. Corrupted value?")
|
fail("Attempted to go past the end of stream. Corrupted value?")
|
||||||
}
|
}
|
||||||
yaml_event_delete(&p.event)
|
yaml_event_delete(&p.event)
|
||||||
}
|
}
|
||||||
@@ -89,7 +91,7 @@ func (p *parser) fail() {
|
|||||||
} else {
|
} else {
|
||||||
msg = "Unknown problem parsing YAML content"
|
msg = "Unknown problem parsing YAML content"
|
||||||
}
|
}
|
||||||
panic(where + msg)
|
fail(where + msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) anchor(n *node, anchor []byte) {
|
func (p *parser) anchor(n *node, anchor []byte) {
|
||||||
@@ -114,10 +116,9 @@ func (p *parser) parse() *node {
|
|||||||
// Happens when attempting to decode an empty buffer.
|
// Happens when attempting to decode an empty buffer.
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
panic("Attempted to parse unknown event: " +
|
panic("Attempted to parse unknown event: " + strconv.Itoa(int(p.event.typ)))
|
||||||
strconv.Itoa(int(p.event.typ)))
|
|
||||||
}
|
}
|
||||||
panic("Unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) node(kind int) *node {
|
func (p *parser) node(kind int) *node {
|
||||||
@@ -135,8 +136,7 @@ func (p *parser) document() *node {
|
|||||||
p.skip()
|
p.skip()
|
||||||
n.children = append(n.children, p.parse())
|
n.children = append(n.children, p.parse())
|
||||||
if p.event.typ != yaml_DOCUMENT_END_EVENT {
|
if p.event.typ != yaml_DOCUMENT_END_EVENT {
|
||||||
panic("Expected end of document event but got " +
|
panic("Expected end of document event but got " + strconv.Itoa(int(p.event.typ)))
|
||||||
strconv.Itoa(int(p.event.typ)))
|
|
||||||
}
|
}
|
||||||
p.skip()
|
p.skip()
|
||||||
return n
|
return n
|
||||||
@@ -218,7 +218,7 @@ func (d *decoder) setter(tag string, out *reflect.Value, good *bool) (set func()
|
|||||||
var arg interface{}
|
var arg interface{}
|
||||||
*out = reflect.ValueOf(&arg).Elem()
|
*out = reflect.ValueOf(&arg).Elem()
|
||||||
return func() {
|
return func() {
|
||||||
*good = setter.SetYAML(tag, arg)
|
*good = setter.SetYAML(shortTag(tag), arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +226,7 @@ func (d *decoder) setter(tag string, out *reflect.Value, good *bool) (set func()
|
|||||||
for again {
|
for again {
|
||||||
again = false
|
again = false
|
||||||
setter, _ := (*out).Interface().(Setter)
|
setter, _ := (*out).Interface().(Setter)
|
||||||
if tag != "!!null" || setter != nil {
|
if tag != yaml_NULL_TAG || setter != nil {
|
||||||
if pv := (*out); pv.Kind() == reflect.Ptr {
|
if pv := (*out); pv.Kind() == reflect.Ptr {
|
||||||
if pv.IsNil() {
|
if pv.IsNil() {
|
||||||
*out = reflect.New(pv.Type().Elem()).Elem()
|
*out = reflect.New(pv.Type().Elem()).Elem()
|
||||||
@@ -242,7 +242,7 @@ func (d *decoder) setter(tag string, out *reflect.Value, good *bool) (set func()
|
|||||||
var arg interface{}
|
var arg interface{}
|
||||||
*out = reflect.ValueOf(&arg).Elem()
|
*out = reflect.ValueOf(&arg).Elem()
|
||||||
return func() {
|
return func() {
|
||||||
*good = setter.SetYAML(tag, arg)
|
*good = setter.SetYAML(shortTag(tag), arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,10 +279,10 @@ func (d *decoder) document(n *node, out reflect.Value) (good bool) {
|
|||||||
func (d *decoder) alias(n *node, out reflect.Value) (good bool) {
|
func (d *decoder) alias(n *node, out reflect.Value) (good bool) {
|
||||||
an, ok := d.doc.anchors[n.value]
|
an, ok := d.doc.anchors[n.value]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("Unknown anchor '" + n.value + "' referenced")
|
fail("Unknown anchor '" + n.value + "' referenced")
|
||||||
}
|
}
|
||||||
if d.aliases[n.value] {
|
if d.aliases[n.value] {
|
||||||
panic("Anchor '" + n.value + "' value contains itself")
|
fail("Anchor '" + n.value + "' value contains itself")
|
||||||
}
|
}
|
||||||
d.aliases[n.value] = true
|
d.aliases[n.value] = true
|
||||||
good = d.unmarshal(an, out)
|
good = d.unmarshal(an, out)
|
||||||
@@ -290,23 +290,50 @@ func (d *decoder) alias(n *node, out reflect.Value) (good bool) {
|
|||||||
return good
|
return good
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var zeroValue reflect.Value
|
||||||
|
|
||||||
|
func resetMap(out reflect.Value) {
|
||||||
|
for _, k := range out.MapKeys() {
|
||||||
|
out.SetMapIndex(k, zeroValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var durationType = reflect.TypeOf(time.Duration(0))
|
var durationType = reflect.TypeOf(time.Duration(0))
|
||||||
|
|
||||||
func (d *decoder) scalar(n *node, out reflect.Value) (good bool) {
|
func (d *decoder) scalar(n *node, out reflect.Value) (good bool) {
|
||||||
var tag string
|
var tag string
|
||||||
var resolved interface{}
|
var resolved interface{}
|
||||||
if n.tag == "" && !n.implicit {
|
if n.tag == "" && !n.implicit {
|
||||||
tag = "!!str"
|
tag = yaml_STR_TAG
|
||||||
resolved = n.value
|
resolved = n.value
|
||||||
} else {
|
} else {
|
||||||
tag, resolved = resolve(n.tag, n.value)
|
tag, resolved = resolve(n.tag, n.value)
|
||||||
|
if tag == yaml_BINARY_TAG {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(resolved.(string))
|
||||||
|
if err != nil {
|
||||||
|
fail("!!binary value contains invalid base64 data")
|
||||||
|
}
|
||||||
|
resolved = string(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if set := d.setter(tag, &out, &good); set != nil {
|
if set := d.setter(tag, &out, &good); set != nil {
|
||||||
defer set()
|
defer set()
|
||||||
}
|
}
|
||||||
|
if resolved == nil {
|
||||||
|
if out.Kind() == reflect.Map && !out.CanAddr() {
|
||||||
|
resetMap(out)
|
||||||
|
} else {
|
||||||
|
out.Set(reflect.Zero(out.Type()))
|
||||||
|
}
|
||||||
|
good = true
|
||||||
|
return
|
||||||
|
}
|
||||||
switch out.Kind() {
|
switch out.Kind() {
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
if resolved != nil {
|
if tag == yaml_BINARY_TAG {
|
||||||
|
out.SetString(resolved.(string))
|
||||||
|
good = true
|
||||||
|
} else if resolved != nil {
|
||||||
out.SetString(n.value)
|
out.SetString(n.value)
|
||||||
good = true
|
good = true
|
||||||
}
|
}
|
||||||
@@ -380,17 +407,11 @@ func (d *decoder) scalar(n *node, out reflect.Value) (good bool) {
|
|||||||
good = true
|
good = true
|
||||||
}
|
}
|
||||||
case reflect.Ptr:
|
case reflect.Ptr:
|
||||||
switch resolved.(type) {
|
if out.Type().Elem() == reflect.TypeOf(resolved) {
|
||||||
case nil:
|
elem := reflect.New(out.Type().Elem())
|
||||||
out.Set(reflect.Zero(out.Type()))
|
elem.Elem().Set(reflect.ValueOf(resolved))
|
||||||
|
out.Set(elem)
|
||||||
good = true
|
good = true
|
||||||
default:
|
|
||||||
if out.Type().Elem() == reflect.TypeOf(resolved) {
|
|
||||||
elem := reflect.New(out.Type().Elem())
|
|
||||||
elem.Elem().Set(reflect.ValueOf(resolved))
|
|
||||||
out.Set(elem)
|
|
||||||
good = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return good
|
return good
|
||||||
@@ -404,7 +425,7 @@ func settableValueOf(i interface{}) reflect.Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *decoder) sequence(n *node, out reflect.Value) (good bool) {
|
func (d *decoder) sequence(n *node, out reflect.Value) (good bool) {
|
||||||
if set := d.setter("!!seq", &out, &good); set != nil {
|
if set := d.setter(yaml_SEQ_TAG, &out, &good); set != nil {
|
||||||
defer set()
|
defer set()
|
||||||
}
|
}
|
||||||
var iface reflect.Value
|
var iface reflect.Value
|
||||||
@@ -433,7 +454,7 @@ func (d *decoder) sequence(n *node, out reflect.Value) (good bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *decoder) mapping(n *node, out reflect.Value) (good bool) {
|
func (d *decoder) mapping(n *node, out reflect.Value) (good bool) {
|
||||||
if set := d.setter("!!map", &out, &good); set != nil {
|
if set := d.setter(yaml_MAP_TAG, &out, &good); set != nil {
|
||||||
defer set()
|
defer set()
|
||||||
}
|
}
|
||||||
if out.Kind() == reflect.Struct {
|
if out.Kind() == reflect.Struct {
|
||||||
@@ -465,6 +486,13 @@ func (d *decoder) mapping(n *node, out reflect.Value) (good bool) {
|
|||||||
}
|
}
|
||||||
k := reflect.New(kt).Elem()
|
k := reflect.New(kt).Elem()
|
||||||
if d.unmarshal(n.children[i], k) {
|
if d.unmarshal(n.children[i], k) {
|
||||||
|
kkind := k.Kind()
|
||||||
|
if kkind == reflect.Interface {
|
||||||
|
kkind = k.Elem().Kind()
|
||||||
|
}
|
||||||
|
if kkind == reflect.Map || kkind == reflect.Slice {
|
||||||
|
fail(fmt.Sprintf("invalid map key: %#v", k.Interface()))
|
||||||
|
}
|
||||||
e := reflect.New(et).Elem()
|
e := reflect.New(et).Elem()
|
||||||
if d.unmarshal(n.children[i+1], e) {
|
if d.unmarshal(n.children[i+1], e) {
|
||||||
out.SetMapIndex(k, e)
|
out.SetMapIndex(k, e)
|
||||||
@@ -511,28 +539,28 @@ func (d *decoder) merge(n *node, out reflect.Value) {
|
|||||||
case aliasNode:
|
case aliasNode:
|
||||||
an, ok := d.doc.anchors[n.value]
|
an, ok := d.doc.anchors[n.value]
|
||||||
if ok && an.kind != mappingNode {
|
if ok && an.kind != mappingNode {
|
||||||
panic(wantMap)
|
fail(wantMap)
|
||||||
}
|
}
|
||||||
d.unmarshal(n, out)
|
d.unmarshal(n, out)
|
||||||
case sequenceNode:
|
case sequenceNode:
|
||||||
// Step backwards as earlier nodes take precedence.
|
// Step backwards as earlier nodes take precedence.
|
||||||
for i := len(n.children)-1; i >= 0; i-- {
|
for i := len(n.children) - 1; i >= 0; i-- {
|
||||||
ni := n.children[i]
|
ni := n.children[i]
|
||||||
if ni.kind == aliasNode {
|
if ni.kind == aliasNode {
|
||||||
an, ok := d.doc.anchors[ni.value]
|
an, ok := d.doc.anchors[ni.value]
|
||||||
if ok && an.kind != mappingNode {
|
if ok && an.kind != mappingNode {
|
||||||
panic(wantMap)
|
fail(wantMap)
|
||||||
}
|
}
|
||||||
} else if ni.kind != mappingNode {
|
} else if ni.kind != mappingNode {
|
||||||
panic(wantMap)
|
fail(wantMap)
|
||||||
}
|
}
|
||||||
d.unmarshal(ni, out)
|
d.unmarshal(ni, out)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
panic(wantMap)
|
fail(wantMap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isMerge(n *node) bool {
|
func isMerge(n *node) bool {
|
||||||
return n.kind == scalarNode && n.value == "<<" && (n.implicit == true || n.tag == "!!merge" || n.tag == "tag:yaml.org,2002:merge")
|
return n.kind == scalarNode && n.value == "<<" && (n.implicit == true || n.tag == yaml_MERGE_TAG)
|
||||||
}
|
}
|
||||||
|
55
Godeps/_workspace/src/gopkg.in/yaml.v1/decode_test.go
generated
vendored
55
Godeps/_workspace/src/gopkg.in/yaml.v1/decode_test.go
generated
vendored
@@ -2,9 +2,10 @@ package yaml_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
. "gopkg.in/check.v1"
|
. "gopkg.in/check.v1"
|
||||||
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1"
|
"gopkg.in/yaml.v1"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -316,7 +317,10 @@ var unmarshalTests = []struct {
|
|||||||
map[string]*string{"foo": new(string)},
|
map[string]*string{"foo": new(string)},
|
||||||
}, {
|
}, {
|
||||||
"foo: null",
|
"foo: null",
|
||||||
map[string]string{},
|
map[string]string{"foo": ""},
|
||||||
|
}, {
|
||||||
|
"foo: null",
|
||||||
|
map[string]interface{}{"foo": nil},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Ignored field
|
// Ignored field
|
||||||
@@ -377,6 +381,24 @@ var unmarshalTests = []struct {
|
|||||||
"a: <foo>",
|
"a: <foo>",
|
||||||
map[string]string{"a": "<foo>"},
|
map[string]string{"a": "<foo>"},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Base 60 floats are obsolete and unsupported.
|
||||||
|
{
|
||||||
|
"a: 1:1\n",
|
||||||
|
map[string]string{"a": "1:1"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Binary data.
|
||||||
|
{
|
||||||
|
"a: !!binary gIGC\n",
|
||||||
|
map[string]string{"a": "\x80\x81\x82"},
|
||||||
|
}, {
|
||||||
|
"a: !!binary |\n " + strings.Repeat("kJCQ", 17) + "kJ\n CQ\n",
|
||||||
|
map[string]string{"a": strings.Repeat("\x90", 54)},
|
||||||
|
}, {
|
||||||
|
"a: !!binary |\n " + strings.Repeat("A", 70) + "\n ==\n",
|
||||||
|
map[string]string{"a": strings.Repeat("\x00", 52)},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type inlineB struct {
|
type inlineB struct {
|
||||||
@@ -424,12 +446,15 @@ func (s *S) TestUnmarshalNaN(c *C) {
|
|||||||
var unmarshalErrorTests = []struct {
|
var unmarshalErrorTests = []struct {
|
||||||
data, error string
|
data, error string
|
||||||
}{
|
}{
|
||||||
{"v: !!float 'error'", "YAML error: Can't decode !!str 'error' as a !!float"},
|
{"v: !!float 'error'", "YAML error: cannot decode !!str `error` as a !!float"},
|
||||||
{"v: [A,", "YAML error: line 1: did not find expected node content"},
|
{"v: [A,", "YAML error: line 1: did not find expected node content"},
|
||||||
{"v:\n- [A,", "YAML error: line 2: did not find expected node content"},
|
{"v:\n- [A,", "YAML error: line 2: did not find expected node content"},
|
||||||
{"a: *b\n", "YAML error: Unknown anchor 'b' referenced"},
|
{"a: *b\n", "YAML error: Unknown anchor 'b' referenced"},
|
||||||
{"a: &a\n b: *a\n", "YAML error: Anchor 'a' value contains itself"},
|
{"a: &a\n b: *a\n", "YAML error: Anchor 'a' value contains itself"},
|
||||||
{"value: -", "YAML error: block sequence entries are not allowed in this context"},
|
{"value: -", "YAML error: block sequence entries are not allowed in this context"},
|
||||||
|
{"a: !!binary ==", "YAML error: !!binary value contains invalid base64 data"},
|
||||||
|
{"{[.]}", `YAML error: invalid map key: \[\]interface \{\}\{"\."\}`},
|
||||||
|
{"{{.}}", `YAML error: invalid map key: map\[interface\ \{\}\]interface \{\}\{".":interface \{\}\(nil\)\}`},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *S) TestUnmarshalErrors(c *C) {
|
func (s *S) TestUnmarshalErrors(c *C) {
|
||||||
@@ -624,6 +649,30 @@ func (s *S) TestMergeStruct(c *C) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var unmarshalNullTests = []func() interface{}{
|
||||||
|
func() interface{} { var v interface{}; v = "v"; return &v },
|
||||||
|
func() interface{} { var s = "s"; return &s },
|
||||||
|
func() interface{} { var s = "s"; sptr := &s; return &sptr },
|
||||||
|
func() interface{} { var i = 1; return &i },
|
||||||
|
func() interface{} { var i = 1; iptr := &i; return &iptr },
|
||||||
|
func() interface{} { m := map[string]int{"s": 1}; return &m },
|
||||||
|
func() interface{} { m := map[string]int{"s": 1}; return m },
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S) TestUnmarshalNull(c *C) {
|
||||||
|
for _, test := range unmarshalNullTests {
|
||||||
|
item := test()
|
||||||
|
zero := reflect.Zero(reflect.TypeOf(item).Elem()).Interface()
|
||||||
|
err := yaml.Unmarshal([]byte("null"), item)
|
||||||
|
c.Assert(err, IsNil)
|
||||||
|
if reflect.TypeOf(item).Kind() == reflect.Map {
|
||||||
|
c.Assert(reflect.ValueOf(item).Interface(), DeepEquals, reflect.MakeMap(reflect.TypeOf(item)).Interface())
|
||||||
|
} else {
|
||||||
|
c.Assert(reflect.ValueOf(item).Elem().Interface(), DeepEquals, zero)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//var data []byte
|
//var data []byte
|
||||||
//func init() {
|
//func init() {
|
||||||
// var err error
|
// var err error
|
||||||
|
5
Godeps/_workspace/src/gopkg.in/yaml.v1/emitterc.go
generated
vendored
5
Godeps/_workspace/src/gopkg.in/yaml.v1/emitterc.go
generated
vendored
@@ -973,8 +973,8 @@ func yaml_emitter_analyze_tag(emitter *yaml_emitter_t, tag []byte) bool {
|
|||||||
if bytes.HasPrefix(tag, tag_directive.prefix) {
|
if bytes.HasPrefix(tag, tag_directive.prefix) {
|
||||||
emitter.tag_data.handle = tag_directive.handle
|
emitter.tag_data.handle = tag_directive.handle
|
||||||
emitter.tag_data.suffix = tag[len(tag_directive.prefix):]
|
emitter.tag_data.suffix = tag[len(tag_directive.prefix):]
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
emitter.tag_data.suffix = tag
|
emitter.tag_data.suffix = tag
|
||||||
return true
|
return true
|
||||||
@@ -1279,6 +1279,9 @@ func yaml_emitter_write_tag_content(emitter *yaml_emitter_t, value []byte, need_
|
|||||||
for k := 0; k < w; k++ {
|
for k := 0; k < w; k++ {
|
||||||
octet := value[i]
|
octet := value[i]
|
||||||
i++
|
i++
|
||||||
|
if !put(emitter, '%') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
c := octet >> 4
|
c := octet >> 4
|
||||||
if c < 10 {
|
if c < 10 {
|
||||||
|
51
Godeps/_workspace/src/gopkg.in/yaml.v1/encode.go
generated
vendored
51
Godeps/_workspace/src/gopkg.in/yaml.v1/encode.go
generated
vendored
@@ -2,8 +2,10 @@ package yaml
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,14 +52,19 @@ func (e *encoder) must(ok bool) {
|
|||||||
if msg == "" {
|
if msg == "" {
|
||||||
msg = "Unknown problem generating YAML content"
|
msg = "Unknown problem generating YAML content"
|
||||||
}
|
}
|
||||||
panic(msg)
|
fail(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *encoder) marshal(tag string, in reflect.Value) {
|
func (e *encoder) marshal(tag string, in reflect.Value) {
|
||||||
|
if !in.IsValid() {
|
||||||
|
e.nilv()
|
||||||
|
return
|
||||||
|
}
|
||||||
var value interface{}
|
var value interface{}
|
||||||
if getter, ok := in.Interface().(Getter); ok {
|
if getter, ok := in.Interface().(Getter); ok {
|
||||||
tag, value = getter.GetYAML()
|
tag, value = getter.GetYAML()
|
||||||
|
tag = longTag(tag)
|
||||||
if value == nil {
|
if value == nil {
|
||||||
e.nilv()
|
e.nilv()
|
||||||
return
|
return
|
||||||
@@ -98,7 +105,7 @@ func (e *encoder) marshal(tag string, in reflect.Value) {
|
|||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
e.boolv(tag, in)
|
e.boolv(tag, in)
|
||||||
default:
|
default:
|
||||||
panic("Can't marshal type yet: " + in.Type().String())
|
panic("Can't marshal type: " + in.Type().String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,11 +174,46 @@ func (e *encoder) slicev(tag string, in reflect.Value) {
|
|||||||
e.emit()
|
e.emit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isBase60 returns whether s is in base 60 notation as defined in YAML 1.1.
|
||||||
|
//
|
||||||
|
// The base 60 float notation in YAML 1.1 is a terrible idea and is unsupported
|
||||||
|
// in YAML 1.2 and by this package, but these should be marshalled quoted for
|
||||||
|
// the time being for compatibility with other parsers.
|
||||||
|
func isBase60Float(s string) (result bool) {
|
||||||
|
// Fast path.
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
c := s[0]
|
||||||
|
if !(c == '+' || c == '-' || c >= '0' && c <= '9') || strings.IndexByte(s, ':') < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Do the full match.
|
||||||
|
return base60float.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// From http://yaml.org/type/float.html, except the regular expression there
|
||||||
|
// is bogus. In practice parsers do not enforce the "\.[0-9_]*" suffix.
|
||||||
|
var base60float = regexp.MustCompile(`^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+(?:\.[0-9_]*)?$`)
|
||||||
|
|
||||||
func (e *encoder) stringv(tag string, in reflect.Value) {
|
func (e *encoder) stringv(tag string, in reflect.Value) {
|
||||||
var style yaml_scalar_style_t
|
var style yaml_scalar_style_t
|
||||||
s := in.String()
|
s := in.String()
|
||||||
if rtag, _ := resolve("", s); rtag != "!!str" {
|
rtag, rs := resolve("", s)
|
||||||
|
if rtag == yaml_BINARY_TAG {
|
||||||
|
if tag == "" || tag == yaml_STR_TAG {
|
||||||
|
tag = rtag
|
||||||
|
s = rs.(string)
|
||||||
|
} else if tag == yaml_BINARY_TAG {
|
||||||
|
fail("explicitly tagged !!binary data must be base64-encoded")
|
||||||
|
} else {
|
||||||
|
fail("cannot marshal invalid UTF-8 data as " + shortTag(tag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tag == "" && (rtag != yaml_STR_TAG || isBase60Float(s)) {
|
||||||
style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
|
style = yaml_DOUBLE_QUOTED_SCALAR_STYLE
|
||||||
|
} else if strings.Contains(s, "\n") {
|
||||||
|
style = yaml_LITERAL_SCALAR_STYLE
|
||||||
} else {
|
} else {
|
||||||
style = yaml_PLAIN_SCALAR_STYLE
|
style = yaml_PLAIN_SCALAR_STYLE
|
||||||
}
|
}
|
||||||
@@ -218,9 +260,6 @@ func (e *encoder) nilv() {
|
|||||||
|
|
||||||
func (e *encoder) emitScalar(value, anchor, tag string, style yaml_scalar_style_t) {
|
func (e *encoder) emitScalar(value, anchor, tag string, style yaml_scalar_style_t) {
|
||||||
implicit := tag == ""
|
implicit := tag == ""
|
||||||
if !implicit {
|
|
||||||
style = yaml_PLAIN_SCALAR_STYLE
|
|
||||||
}
|
|
||||||
e.must(yaml_scalar_event_initialize(&e.event, []byte(anchor), []byte(tag), []byte(value), implicit, implicit, style))
|
e.must(yaml_scalar_event_initialize(&e.event, []byte(anchor), []byte(tag), []byte(value), implicit, implicit, style))
|
||||||
e.emit()
|
e.emit()
|
||||||
}
|
}
|
||||||
|
71
Godeps/_workspace/src/gopkg.in/yaml.v1/encode_test.go
generated
vendored
71
Godeps/_workspace/src/gopkg.in/yaml.v1/encode_test.go
generated
vendored
@@ -2,12 +2,13 @@ package yaml_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1"
|
|
||||||
. "gopkg.in/check.v1"
|
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
. "gopkg.in/check.v1"
|
||||||
|
"gopkg.in/yaml.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var marshalIntTest = 123
|
var marshalIntTest = 123
|
||||||
@@ -17,6 +18,9 @@ var marshalTests = []struct {
|
|||||||
data string
|
data string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
nil,
|
||||||
|
"null\n",
|
||||||
|
}, {
|
||||||
&struct{}{},
|
&struct{}{},
|
||||||
"{}\n",
|
"{}\n",
|
||||||
}, {
|
}, {
|
||||||
@@ -87,7 +91,7 @@ var marshalTests = []struct {
|
|||||||
"v:\n- A\n- B\n",
|
"v:\n- A\n- B\n",
|
||||||
}, {
|
}, {
|
||||||
map[string][]string{"v": []string{"A", "B\nC"}},
|
map[string][]string{"v": []string{"A", "B\nC"}},
|
||||||
"v:\n- A\n- 'B\n\n C'\n",
|
"v:\n- A\n- |-\n B\n C\n",
|
||||||
}, {
|
}, {
|
||||||
map[string][]interface{}{"v": []interface{}{"A", 1, map[string][]int{"B": []int{2, 3}}}},
|
map[string][]interface{}{"v": []interface{}{"A", 1, map[string][]int{"B": []int{2, 3}}}},
|
||||||
"v:\n- A\n- 1\n- B:\n - 2\n - 3\n",
|
"v:\n- A\n- 1\n- B:\n - 2\n - 3\n",
|
||||||
@@ -220,11 +224,39 @@ var marshalTests = []struct {
|
|||||||
"a: 3s\n",
|
"a: 3s\n",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Issue #24.
|
// Issue #24: bug in map merging logic.
|
||||||
{
|
{
|
||||||
map[string]string{"a": "<foo>"},
|
map[string]string{"a": "<foo>"},
|
||||||
"a: <foo>\n",
|
"a: <foo>\n",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Issue #34: marshal unsupported base 60 floats quoted for compatibility
|
||||||
|
// with old YAML 1.1 parsers.
|
||||||
|
{
|
||||||
|
map[string]string{"a": "1:1"},
|
||||||
|
"a: \"1:1\"\n",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Binary data.
|
||||||
|
{
|
||||||
|
map[string]string{"a": "\x00"},
|
||||||
|
"a: \"\\0\"\n",
|
||||||
|
}, {
|
||||||
|
map[string]string{"a": "\x80\x81\x82"},
|
||||||
|
"a: !!binary gIGC\n",
|
||||||
|
}, {
|
||||||
|
map[string]string{"a": strings.Repeat("\x90", 54)},
|
||||||
|
"a: !!binary |\n " + strings.Repeat("kJCQ", 17) + "kJ\n CQ\n",
|
||||||
|
}, {
|
||||||
|
map[string]interface{}{"a": typeWithGetter{"!!str", "\x80\x81\x82"}},
|
||||||
|
"a: !!binary gIGC\n",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Escaping of tags.
|
||||||
|
{
|
||||||
|
map[string]interface{}{"a": typeWithGetter{"foo!bar", 1}},
|
||||||
|
"a: !<foo%21bar> 1\n",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *S) TestMarshal(c *C) {
|
func (s *S) TestMarshal(c *C) {
|
||||||
@@ -238,20 +270,29 @@ func (s *S) TestMarshal(c *C) {
|
|||||||
var marshalErrorTests = []struct {
|
var marshalErrorTests = []struct {
|
||||||
value interface{}
|
value interface{}
|
||||||
error string
|
error string
|
||||||
}{
|
panic string
|
||||||
{
|
}{{
|
||||||
&struct {
|
value: &struct {
|
||||||
B int
|
B int
|
||||||
inlineB ",inline"
|
inlineB ",inline"
|
||||||
}{1, inlineB{2, inlineC{3}}},
|
}{1, inlineB{2, inlineC{3}}},
|
||||||
`Duplicated key 'b' in struct struct \{ B int; .*`,
|
panic: `Duplicated key 'b' in struct struct \{ B int; .*`,
|
||||||
},
|
}, {
|
||||||
}
|
value: typeWithGetter{"!!binary", "\x80"},
|
||||||
|
error: "YAML error: explicitly tagged !!binary data must be base64-encoded",
|
||||||
|
}, {
|
||||||
|
value: typeWithGetter{"!!float", "\x80"},
|
||||||
|
error: `YAML error: cannot marshal invalid UTF-8 data as !!float`,
|
||||||
|
}}
|
||||||
|
|
||||||
func (s *S) TestMarshalErrors(c *C) {
|
func (s *S) TestMarshalErrors(c *C) {
|
||||||
for _, item := range marshalErrorTests {
|
for _, item := range marshalErrorTests {
|
||||||
_, err := yaml.Marshal(item.value)
|
if item.panic != "" {
|
||||||
c.Assert(err, ErrorMatches, item.error)
|
c.Assert(func() { yaml.Marshal(item.value) }, PanicMatches, item.panic)
|
||||||
|
} else {
|
||||||
|
_, err := yaml.Marshal(item.value)
|
||||||
|
c.Assert(err, ErrorMatches, item.error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
181
Godeps/_workspace/src/gopkg.in/yaml.v1/resolve.go
generated
vendored
181
Godeps/_workspace/src/gopkg.in/yaml.v1/resolve.go
generated
vendored
@@ -1,9 +1,12 @@
|
|||||||
package yaml
|
package yaml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: merge, timestamps, base 60 floats, omap.
|
// TODO: merge, timestamps, base 60 floats, omap.
|
||||||
@@ -33,18 +36,18 @@ func init() {
|
|||||||
tag string
|
tag string
|
||||||
l []string
|
l []string
|
||||||
}{
|
}{
|
||||||
{true, "!!bool", []string{"y", "Y", "yes", "Yes", "YES"}},
|
{true, yaml_BOOL_TAG, []string{"y", "Y", "yes", "Yes", "YES"}},
|
||||||
{true, "!!bool", []string{"true", "True", "TRUE"}},
|
{true, yaml_BOOL_TAG, []string{"true", "True", "TRUE"}},
|
||||||
{true, "!!bool", []string{"on", "On", "ON"}},
|
{true, yaml_BOOL_TAG, []string{"on", "On", "ON"}},
|
||||||
{false, "!!bool", []string{"n", "N", "no", "No", "NO"}},
|
{false, yaml_BOOL_TAG, []string{"n", "N", "no", "No", "NO"}},
|
||||||
{false, "!!bool", []string{"false", "False", "FALSE"}},
|
{false, yaml_BOOL_TAG, []string{"false", "False", "FALSE"}},
|
||||||
{false, "!!bool", []string{"off", "Off", "OFF"}},
|
{false, yaml_BOOL_TAG, []string{"off", "Off", "OFF"}},
|
||||||
{nil, "!!null", []string{"~", "null", "Null", "NULL"}},
|
{nil, yaml_NULL_TAG, []string{"", "~", "null", "Null", "NULL"}},
|
||||||
{math.NaN(), "!!float", []string{".nan", ".NaN", ".NAN"}},
|
{math.NaN(), yaml_FLOAT_TAG, []string{".nan", ".NaN", ".NAN"}},
|
||||||
{math.Inf(+1), "!!float", []string{".inf", ".Inf", ".INF"}},
|
{math.Inf(+1), yaml_FLOAT_TAG, []string{".inf", ".Inf", ".INF"}},
|
||||||
{math.Inf(+1), "!!float", []string{"+.inf", "+.Inf", "+.INF"}},
|
{math.Inf(+1), yaml_FLOAT_TAG, []string{"+.inf", "+.Inf", "+.INF"}},
|
||||||
{math.Inf(-1), "!!float", []string{"-.inf", "-.Inf", "-.INF"}},
|
{math.Inf(-1), yaml_FLOAT_TAG, []string{"-.inf", "-.Inf", "-.INF"}},
|
||||||
{"<<", "!!merge", []string{"<<"}},
|
{"<<", yaml_MERGE_TAG, []string{"<<"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
m := resolveMap
|
m := resolveMap
|
||||||
@@ -58,90 +61,130 @@ func init() {
|
|||||||
const longTagPrefix = "tag:yaml.org,2002:"
|
const longTagPrefix = "tag:yaml.org,2002:"
|
||||||
|
|
||||||
func shortTag(tag string) string {
|
func shortTag(tag string) string {
|
||||||
|
// TODO This can easily be made faster and produce less garbage.
|
||||||
if strings.HasPrefix(tag, longTagPrefix) {
|
if strings.HasPrefix(tag, longTagPrefix) {
|
||||||
return "!!" + tag[len(longTagPrefix):]
|
return "!!" + tag[len(longTagPrefix):]
|
||||||
}
|
}
|
||||||
return tag
|
return tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func longTag(tag string) string {
|
||||||
|
if strings.HasPrefix(tag, "!!") {
|
||||||
|
return longTagPrefix + tag[2:]
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
func resolvableTag(tag string) bool {
|
func resolvableTag(tag string) bool {
|
||||||
switch tag {
|
switch tag {
|
||||||
case "", "!!str", "!!bool", "!!int", "!!float", "!!null":
|
case "", yaml_STR_TAG, yaml_BOOL_TAG, yaml_INT_TAG, yaml_FLOAT_TAG, yaml_NULL_TAG:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolve(tag string, in string) (rtag string, out interface{}) {
|
func resolve(tag string, in string) (rtag string, out interface{}) {
|
||||||
tag = shortTag(tag)
|
|
||||||
if !resolvableTag(tag) {
|
if !resolvableTag(tag) {
|
||||||
return tag, in
|
return tag, in
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if tag != "" && tag != rtag {
|
switch tag {
|
||||||
panic("Can't decode " + rtag + " '" + in + "' as a " + tag)
|
case "", rtag, yaml_STR_TAG, yaml_BINARY_TAG:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
fail(fmt.Sprintf("cannot decode %s `%s` as a %s", shortTag(rtag), in, shortTag(tag)))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if in == "" {
|
// Any data is accepted as a !!str or !!binary.
|
||||||
return "!!null", nil
|
// Otherwise, the prefix is enough of a hint about what it might be.
|
||||||
|
hint := byte('N')
|
||||||
|
if in != "" {
|
||||||
|
hint = resolveTable[in[0]]
|
||||||
}
|
}
|
||||||
|
if hint != 0 && tag != yaml_STR_TAG && tag != yaml_BINARY_TAG {
|
||||||
c := resolveTable[in[0]]
|
// Handle things we can lookup in a map.
|
||||||
if c == 0 {
|
if item, ok := resolveMap[in]; ok {
|
||||||
// It's a string for sure. Nothing to do.
|
return item.tag, item.value
|
||||||
return "!!str", in
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle things we can lookup in a map.
|
|
||||||
if item, ok := resolveMap[in]; ok {
|
|
||||||
return item.tag, item.value
|
|
||||||
}
|
|
||||||
|
|
||||||
switch c {
|
|
||||||
case 'M':
|
|
||||||
// We've already checked the map above.
|
|
||||||
|
|
||||||
case '.':
|
|
||||||
// Not in the map, so maybe a normal float.
|
|
||||||
floatv, err := strconv.ParseFloat(in, 64)
|
|
||||||
if err == nil {
|
|
||||||
return "!!float", floatv
|
|
||||||
}
|
}
|
||||||
// XXX Handle base 60 floats here (WTF!)
|
|
||||||
|
|
||||||
case 'D', 'S':
|
// Base 60 floats are a bad idea, were dropped in YAML 1.2, and
|
||||||
// Int, float, or timestamp.
|
// are purposefully unsupported here. They're still quoted on
|
||||||
plain := strings.Replace(in, "_", "", -1)
|
// the way out for compatibility with other parser, though.
|
||||||
intv, err := strconv.ParseInt(plain, 0, 64)
|
|
||||||
if err == nil {
|
switch hint {
|
||||||
if intv == int64(int(intv)) {
|
case 'M':
|
||||||
return "!!int", int(intv)
|
// We've already checked the map above.
|
||||||
} else {
|
|
||||||
return "!!int", intv
|
case '.':
|
||||||
}
|
// Not in the map, so maybe a normal float.
|
||||||
}
|
floatv, err := strconv.ParseFloat(in, 64)
|
||||||
floatv, err := strconv.ParseFloat(plain, 64)
|
|
||||||
if err == nil {
|
|
||||||
return "!!float", floatv
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(plain, "0b") {
|
|
||||||
intv, err := strconv.ParseInt(plain[2:], 2, 64)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return "!!int", int(intv)
|
return yaml_FLOAT_TAG, floatv
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(plain, "-0b") {
|
|
||||||
intv, err := strconv.ParseInt(plain[3:], 2, 64)
|
|
||||||
if err == nil {
|
|
||||||
return "!!int", -int(intv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// XXX Handle timestamps here.
|
|
||||||
|
|
||||||
default:
|
case 'D', 'S':
|
||||||
panic("resolveTable item not yet handled: " +
|
// Int, float, or timestamp.
|
||||||
string([]byte{c}) + " (with " + in + ")")
|
plain := strings.Replace(in, "_", "", -1)
|
||||||
|
intv, err := strconv.ParseInt(plain, 0, 64)
|
||||||
|
if err == nil {
|
||||||
|
if intv == int64(int(intv)) {
|
||||||
|
return yaml_INT_TAG, int(intv)
|
||||||
|
} else {
|
||||||
|
return yaml_INT_TAG, intv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
floatv, err := strconv.ParseFloat(plain, 64)
|
||||||
|
if err == nil {
|
||||||
|
return yaml_FLOAT_TAG, floatv
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(plain, "0b") {
|
||||||
|
intv, err := strconv.ParseInt(plain[2:], 2, 64)
|
||||||
|
if err == nil {
|
||||||
|
return yaml_INT_TAG, int(intv)
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(plain, "-0b") {
|
||||||
|
intv, err := strconv.ParseInt(plain[3:], 2, 64)
|
||||||
|
if err == nil {
|
||||||
|
return yaml_INT_TAG, -int(intv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// XXX Handle timestamps here.
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("resolveTable item not yet handled: " + string(rune(hint)) + " (with " + in + ")")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return "!!str", in
|
if tag == yaml_BINARY_TAG {
|
||||||
|
return yaml_BINARY_TAG, in
|
||||||
|
}
|
||||||
|
if utf8.ValidString(in) {
|
||||||
|
return yaml_STR_TAG, in
|
||||||
|
}
|
||||||
|
return yaml_BINARY_TAG, encodeBase64(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeBase64 encodes s as base64 that is broken up into multiple lines
|
||||||
|
// as appropriate for the resulting length.
|
||||||
|
func encodeBase64(s string) string {
|
||||||
|
const lineLen = 70
|
||||||
|
encLen := base64.StdEncoding.EncodedLen(len(s))
|
||||||
|
lines := encLen/lineLen + 1
|
||||||
|
buf := make([]byte, encLen*2+lines)
|
||||||
|
in := buf[0:encLen]
|
||||||
|
out := buf[encLen:]
|
||||||
|
base64.StdEncoding.Encode(in, []byte(s))
|
||||||
|
k := 0
|
||||||
|
for i := 0; i < len(in); i += lineLen {
|
||||||
|
j := i + lineLen
|
||||||
|
if j > len(in) {
|
||||||
|
j = len(in)
|
||||||
|
}
|
||||||
|
k += copy(out[k:], in[i:j])
|
||||||
|
if lines > 1 {
|
||||||
|
out[k] = '\n'
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(out[:k])
|
||||||
}
|
}
|
||||||
|
41
Godeps/_workspace/src/gopkg.in/yaml.v1/yaml.go
generated
vendored
41
Godeps/_workspace/src/gopkg.in/yaml.v1/yaml.go
generated
vendored
@@ -10,23 +10,20 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type yamlError string
|
||||||
|
|
||||||
|
func fail(msg string) {
|
||||||
|
panic(yamlError(msg))
|
||||||
|
}
|
||||||
|
|
||||||
func handleErr(err *error) {
|
func handleErr(err *error) {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
if _, ok := r.(runtime.Error); ok {
|
if e, ok := r.(yamlError); ok {
|
||||||
panic(r)
|
*err = errors.New("YAML error: " + string(e))
|
||||||
} else if _, ok := r.(*reflect.ValueError); ok {
|
|
||||||
panic(r)
|
|
||||||
} else if _, ok := r.(externalPanic); ok {
|
|
||||||
panic(r)
|
|
||||||
} else if s, ok := r.(string); ok {
|
|
||||||
*err = errors.New("YAML error: " + s)
|
|
||||||
} else if e, ok := r.(error); ok {
|
|
||||||
*err = e
|
|
||||||
} else {
|
} else {
|
||||||
panic(r)
|
panic(r)
|
||||||
}
|
}
|
||||||
@@ -78,7 +75,7 @@ type Getter interface {
|
|||||||
// F int `yaml:"a,omitempty"`
|
// F int `yaml:"a,omitempty"`
|
||||||
// B int
|
// B int
|
||||||
// }
|
// }
|
||||||
// var T t
|
// var t T
|
||||||
// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t)
|
// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t)
|
||||||
//
|
//
|
||||||
// See the documentation of Marshal for the format of tags and a list of
|
// See the documentation of Marshal for the format of tags and a list of
|
||||||
@@ -91,7 +88,11 @@ func Unmarshal(in []byte, out interface{}) (err error) {
|
|||||||
defer p.destroy()
|
defer p.destroy()
|
||||||
node := p.parse()
|
node := p.parse()
|
||||||
if node != nil {
|
if node != nil {
|
||||||
d.unmarshal(node, reflect.ValueOf(out))
|
v := reflect.ValueOf(out)
|
||||||
|
if v.Kind() == reflect.Ptr && !v.IsNil() {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
d.unmarshal(node, v)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -174,12 +175,6 @@ type fieldInfo struct {
|
|||||||
var structMap = make(map[reflect.Type]*structInfo)
|
var structMap = make(map[reflect.Type]*structInfo)
|
||||||
var fieldMapMutex sync.RWMutex
|
var fieldMapMutex sync.RWMutex
|
||||||
|
|
||||||
type externalPanic string
|
|
||||||
|
|
||||||
func (e externalPanic) String() string {
|
|
||||||
return string(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStructInfo(st reflect.Type) (*structInfo, error) {
|
func getStructInfo(st reflect.Type) (*structInfo, error) {
|
||||||
fieldMapMutex.RLock()
|
fieldMapMutex.RLock()
|
||||||
sinfo, found := structMap[st]
|
sinfo, found := structMap[st]
|
||||||
@@ -220,8 +215,7 @@ func getStructInfo(st reflect.Type) (*structInfo, error) {
|
|||||||
case "inline":
|
case "inline":
|
||||||
inline = true
|
inline = true
|
||||||
default:
|
default:
|
||||||
msg := fmt.Sprintf("Unsupported flag %q in tag %q of type %s", flag, tag, st)
|
return nil, errors.New(fmt.Sprintf("Unsupported flag %q in tag %q of type %s", flag, tag, st))
|
||||||
panic(externalPanic(msg))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tag = fields[0]
|
tag = fields[0]
|
||||||
@@ -229,6 +223,7 @@ func getStructInfo(st reflect.Type) (*structInfo, error) {
|
|||||||
|
|
||||||
if inline {
|
if inline {
|
||||||
switch field.Type.Kind() {
|
switch field.Type.Kind() {
|
||||||
|
// TODO: Implement support for inline maps.
|
||||||
//case reflect.Map:
|
//case reflect.Map:
|
||||||
// if inlineMap >= 0 {
|
// if inlineMap >= 0 {
|
||||||
// return nil, errors.New("Multiple ,inline maps in struct " + st.String())
|
// return nil, errors.New("Multiple ,inline maps in struct " + st.String())
|
||||||
@@ -256,8 +251,8 @@ func getStructInfo(st reflect.Type) (*structInfo, error) {
|
|||||||
fieldsList = append(fieldsList, finfo)
|
fieldsList = append(fieldsList, finfo)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
//panic("Option ,inline needs a struct value or map field")
|
//return nil, errors.New("Option ,inline needs a struct value or map field")
|
||||||
panic("Option ,inline needs a struct value field")
|
return nil, errors.New("Option ,inline needs a struct value field")
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
4
Godeps/_workspace/src/gopkg.in/yaml.v1/yamlh.go
generated
vendored
4
Godeps/_workspace/src/gopkg.in/yaml.v1/yamlh.go
generated
vendored
@@ -294,6 +294,10 @@ const (
|
|||||||
yaml_SEQ_TAG = "tag:yaml.org,2002:seq" // The tag !!seq is used to denote sequences.
|
yaml_SEQ_TAG = "tag:yaml.org,2002:seq" // The tag !!seq is used to denote sequences.
|
||||||
yaml_MAP_TAG = "tag:yaml.org,2002:map" // The tag !!map is used to denote mapping.
|
yaml_MAP_TAG = "tag:yaml.org,2002:map" // The tag !!map is used to denote mapping.
|
||||||
|
|
||||||
|
// Not in original libyaml.
|
||||||
|
yaml_BINARY_TAG = "tag:yaml.org,2002:binary"
|
||||||
|
yaml_MERGE_TAG = "tag:yaml.org,2002:merge"
|
||||||
|
|
||||||
yaml_DEFAULT_SCALAR_TAG = yaml_STR_TAG // The default scalar tag is !!str.
|
yaml_DEFAULT_SCALAR_TAG = yaml_STR_TAG // The default scalar tag is !!str.
|
||||||
yaml_DEFAULT_SEQUENCE_TAG = yaml_SEQ_TAG // The default sequence tag is !!seq.
|
yaml_DEFAULT_SEQUENCE_TAG = yaml_SEQ_TAG // The default sequence tag is !!seq.
|
||||||
yaml_DEFAULT_MAPPING_TAG = yaml_MAP_TAG // The default mapping tag is !!map.
|
yaml_DEFAULT_MAPPING_TAG = yaml_MAP_TAG // The default mapping tag is !!map.
|
||||||
|
187
config/config.go
Normal file
187
config/config.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 CoreOS, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CloudConfig encapsulates the entire cloud-config configuration file and maps
|
||||||
|
// directly to YAML. Fields that cannot be set in the cloud-config (fields
|
||||||
|
// used for internal use) have the YAML tag '-' so that they aren't marshalled.
|
||||||
|
type CloudConfig struct {
|
||||||
|
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
|
||||||
|
Coreos struct {
|
||||||
|
Etcd Etcd `yaml:"etcd"`
|
||||||
|
Flannel Flannel `yaml:"flannel"`
|
||||||
|
Fleet Fleet `yaml:"fleet"`
|
||||||
|
Locksmith Locksmith `yaml:"locksmith"`
|
||||||
|
OEM OEM `yaml:"oem"`
|
||||||
|
Update Update `yaml:"update"`
|
||||||
|
Units []Unit `yaml:"units"`
|
||||||
|
} `yaml:"coreos"`
|
||||||
|
WriteFiles []File `yaml:"write_files"`
|
||||||
|
Hostname string `yaml:"hostname"`
|
||||||
|
Users []User `yaml:"users"`
|
||||||
|
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
|
||||||
|
NetworkConfigPath string `yaml:"-"`
|
||||||
|
NetworkConfig string `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsCloudConfig(userdata string) bool {
|
||||||
|
header := strings.SplitN(userdata, "\n", 2)[0]
|
||||||
|
|
||||||
|
// Explicitly trim the header so we can handle user-data from
|
||||||
|
// non-unix operating systems. The rest of the file is parsed
|
||||||
|
// by yaml, which correctly handles CRLF.
|
||||||
|
header = strings.TrimSuffix(header, "\r")
|
||||||
|
|
||||||
|
return (header == "#cloud-config")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCloudConfig instantiates a new CloudConfig from the given contents (a
|
||||||
|
// 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
|
||||||
|
ncontents, err := normalizeConfig(contents)
|
||||||
|
if err != nil {
|
||||||
|
return &cfg, err
|
||||||
|
}
|
||||||
|
if err = yaml.Unmarshal(ncontents, &cfg); err != nil {
|
||||||
|
return &cfg, err
|
||||||
|
}
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc CloudConfig) String() string {
|
||||||
|
bytes, err := yaml.Marshal(cc)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
stringified := string(bytes)
|
||||||
|
stringified = fmt.Sprintf("#cloud-config\n%s", stringified)
|
||||||
|
|
||||||
|
return stringified
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns whether or not the parameter is the zero value for its type.
|
||||||
|
// If the parameter is a struct, only the exported fields are considered.
|
||||||
|
func IsZero(c interface{}) bool {
|
||||||
|
return isZero(reflect.ValueOf(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 AssertStructValid(c interface{}) error {
|
||||||
|
ct := reflect.TypeOf(c)
|
||||||
|
cv := reflect.ValueOf(c)
|
||||||
|
for i := 0; i < ct.NumField(); i++ {
|
||||||
|
ft := ct.Field(i)
|
||||||
|
if !isFieldExported(ft) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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:
|
||||||
|
vt := v.Type()
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
if isFieldExported(vt.Field(i)) && !isZero(v.Field(i)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return v.Interface() == reflect.Zero(v.Type()).Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFieldExported(f reflect.StructField) bool {
|
||||||
|
return f.PkgPath == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeConfig(config string) ([]byte, error) {
|
||||||
|
var cfg map[interface{}]interface{}
|
||||||
|
if err := yaml.Unmarshal([]byte(config), &cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return yaml.Marshal(normalizeKeys(cfg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeKeys(m map[interface{}]interface{}) map[interface{}]interface{} {
|
||||||
|
for k, v := range m {
|
||||||
|
if m, ok := m[k].(map[interface{}]interface{}); ok {
|
||||||
|
normalizeKeys(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s, ok := m[k].([]interface{}); ok {
|
||||||
|
for _, e := range s {
|
||||||
|
if m, ok := e.(map[interface{}]interface{}); ok {
|
||||||
|
normalizeKeys(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(m, k)
|
||||||
|
m[strings.Replace(fmt.Sprint(k), "-", "_", -1)] = v
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
503
config/config_test.go
Normal file
503
config/config_test.go
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsZero(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
c interface{}
|
||||||
|
empty bool
|
||||||
|
}{
|
||||||
|
{struct{}{}, true},
|
||||||
|
{struct{ a, b string }{}, true},
|
||||||
|
{struct{ A, b string }{}, true},
|
||||||
|
{struct{ A, B string }{}, true},
|
||||||
|
{struct{ A string }{A: "hello"}, false},
|
||||||
|
{struct{ A int }{}, true},
|
||||||
|
{struct{ A int }{A: 1}, false},
|
||||||
|
} {
|
||||||
|
if empty := IsZero(tt.c); tt.empty != empty {
|
||||||
|
t.Errorf("bad result (%q): want %t, got %t", tt.c, tt.empty, empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAssertStructValid(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
c interface{}
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{struct{}{}, nil},
|
||||||
|
{struct {
|
||||||
|
A, b string `valid:"1,2"`
|
||||||
|
}{}, nil},
|
||||||
|
{struct {
|
||||||
|
A, b string `valid:"1,2"`
|
||||||
|
}{A: "1", b: "2"}, nil},
|
||||||
|
{struct {
|
||||||
|
A, b string `valid:"1,2"`
|
||||||
|
}{A: "1", b: "hello"}, nil},
|
||||||
|
{struct {
|
||||||
|
A, b string `valid:"1,2"`
|
||||||
|
}{A: "hello", b: "2"}, &ErrorValid{Value: "hello", Field: "A", Valid: []string{"1", "2"}}},
|
||||||
|
{struct {
|
||||||
|
A, b int `valid:"1,2"`
|
||||||
|
}{}, nil},
|
||||||
|
{struct {
|
||||||
|
A, b int `valid:"1,2"`
|
||||||
|
}{A: 1, b: 2}, nil},
|
||||||
|
{struct {
|
||||||
|
A, b int `valid:"1,2"`
|
||||||
|
}{A: 1, b: 9}, nil},
|
||||||
|
{struct {
|
||||||
|
A, b int `valid:"1,2"`
|
||||||
|
}{A: 9, b: 2}, &ErrorValid{Value: "9", Field: "A", Valid: []string{"1", "2"}}},
|
||||||
|
} {
|
||||||
|
if err := AssertStructValid(tt.c); !reflect.DeepEqual(tt.err, err) {
|
||||||
|
t.Errorf("bad result (%q): want %q, got %q", tt.c, tt.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudConfigInvalidKeys(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Fatalf("panic while instantiating CloudConfig with nil keys: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, tt := range []struct {
|
||||||
|
contents string
|
||||||
|
}{
|
||||||
|
{"coreos:"},
|
||||||
|
{"ssh_authorized_keys:"},
|
||||||
|
{"ssh_authorized_keys:\n -"},
|
||||||
|
{"ssh_authorized_keys:\n - 0:"},
|
||||||
|
{"write_files:"},
|
||||||
|
{"write_files:\n -"},
|
||||||
|
{"write_files:\n - 0:"},
|
||||||
|
{"users:"},
|
||||||
|
{"users:\n -"},
|
||||||
|
{"users:\n - 0:"},
|
||||||
|
} {
|
||||||
|
_, err := NewCloudConfig(tt.contents)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error instantiating CloudConfig with invalid keys: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudConfigUnknownKeys(t *testing.T) {
|
||||||
|
contents := `
|
||||||
|
coreos:
|
||||||
|
etcd:
|
||||||
|
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
|
||||||
|
coreos_unknown:
|
||||||
|
foo: "bar"
|
||||||
|
section_unknown:
|
||||||
|
dunno:
|
||||||
|
something
|
||||||
|
bare_unknown:
|
||||||
|
bar
|
||||||
|
write_files:
|
||||||
|
- content: fun
|
||||||
|
path: /var/party
|
||||||
|
file_unknown: nofun
|
||||||
|
users:
|
||||||
|
- name: fry
|
||||||
|
passwd: somehash
|
||||||
|
user_unknown: philip
|
||||||
|
hostname:
|
||||||
|
foo
|
||||||
|
`
|
||||||
|
cfg, err := NewCloudConfig(contents)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error instantiating CloudConfig with unknown keys: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.Hostname != "foo" {
|
||||||
|
t.Fatalf("hostname not correctly set when invalid keys are present")
|
||||||
|
}
|
||||||
|
if cfg.Coreos.Etcd.Discovery != "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877" {
|
||||||
|
t.Fatalf("etcd section not correctly set when invalid keys are present")
|
||||||
|
}
|
||||||
|
if len(cfg.WriteFiles) < 1 || cfg.WriteFiles[0].Content != "fun" || cfg.WriteFiles[0].Path != "/var/party" {
|
||||||
|
t.Fatalf("write_files section not correctly set when invalid keys are present")
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the parsing of a cloud config file "generally works"
|
||||||
|
func TestCloudConfigEmpty(t *testing.T) {
|
||||||
|
cfg, err := NewCloudConfig("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encountered unexpected error :%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := cfg.SSHAuthorizedKeys
|
||||||
|
if len(keys) != 0 {
|
||||||
|
t.Error("Parsed incorrect number of SSH keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.WriteFiles) != 0 {
|
||||||
|
t.Error("Expected zero WriteFiles")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Hostname != "" {
|
||||||
|
t.Errorf("Expected hostname to be empty, got '%s'", cfg.Hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the parsing of a cloud config file "generally works"
|
||||||
|
func TestCloudConfig(t *testing.T) {
|
||||||
|
contents := `
|
||||||
|
coreos:
|
||||||
|
etcd:
|
||||||
|
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
|
||||||
|
update:
|
||||||
|
reboot_strategy: reboot
|
||||||
|
units:
|
||||||
|
- name: 50-eth0.network
|
||||||
|
runtime: yes
|
||||||
|
content: '[Match]
|
||||||
|
|
||||||
|
Name=eth47
|
||||||
|
|
||||||
|
|
||||||
|
[Network]
|
||||||
|
|
||||||
|
Address=10.209.171.177/19
|
||||||
|
|
||||||
|
'
|
||||||
|
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
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- foobar
|
||||||
|
- foobaz
|
||||||
|
write_files:
|
||||||
|
- content: |
|
||||||
|
penny
|
||||||
|
elroy
|
||||||
|
path: /etc/dogepack.conf
|
||||||
|
permissions: '0644'
|
||||||
|
owner: root:dogepack
|
||||||
|
hostname: trontastic
|
||||||
|
`
|
||||||
|
cfg, err := NewCloudConfig(contents)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encountered unexpected error :%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := cfg.SSHAuthorizedKeys
|
||||||
|
if len(keys) != 2 {
|
||||||
|
t.Error("Parsed incorrect number of SSH keys")
|
||||||
|
} else if keys[0] != "foobar" {
|
||||||
|
t.Error("Expected first SSH key to be 'foobar'")
|
||||||
|
} else if keys[1] != "foobaz" {
|
||||||
|
t.Error("Expected first SSH key to be 'foobaz'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.WriteFiles) != 1 {
|
||||||
|
t.Error("Failed to parse correct number of write_files")
|
||||||
|
} else {
|
||||||
|
wf := cfg.WriteFiles[0]
|
||||||
|
if wf.Content != "penny\nelroy\n" {
|
||||||
|
t.Errorf("WriteFile has incorrect contents '%s'", wf.Content)
|
||||||
|
}
|
||||||
|
if wf.Encoding != "" {
|
||||||
|
t.Errorf("WriteFile has incorrect encoding %s", wf.Encoding)
|
||||||
|
}
|
||||||
|
if wf.RawFilePermissions != "0644" {
|
||||||
|
t.Errorf("WriteFile has incorrect permissions %s", wf.RawFilePermissions)
|
||||||
|
}
|
||||||
|
if wf.Path != "/etc/dogepack.conf" {
|
||||||
|
t.Errorf("WriteFile has incorrect path %s", wf.Path)
|
||||||
|
}
|
||||||
|
if wf.Owner != "root:dogepack" {
|
||||||
|
t.Errorf("WriteFile has incorrect owner %s", wf.Owner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.Coreos.Units) != 1 {
|
||||||
|
t.Error("Failed to parse correct number of units")
|
||||||
|
} else {
|
||||||
|
u := cfg.Coreos.Units[0]
|
||||||
|
expect := `[Match]
|
||||||
|
Name=eth47
|
||||||
|
|
||||||
|
[Network]
|
||||||
|
Address=10.209.171.177/19
|
||||||
|
`
|
||||||
|
if u.Content != expect {
|
||||||
|
t.Errorf("Unit has incorrect contents '%s'.\nExpected '%s'.", u.Content, expect)
|
||||||
|
}
|
||||||
|
if u.Runtime != true {
|
||||||
|
t.Errorf("Unit has incorrect runtime value")
|
||||||
|
}
|
||||||
|
if u.Name != "50-eth0.network" {
|
||||||
|
t.Errorf("Unit has incorrect name %s", u.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Coreos.OEM.ID != "rackspace" {
|
||||||
|
t.Errorf("Failed parsing coreos.oem. Expected ID 'rackspace', got %q.", cfg.Coreos.OEM.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Hostname != "trontastic" {
|
||||||
|
t.Errorf("Failed to parse hostname")
|
||||||
|
}
|
||||||
|
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
|
||||||
|
func TestCloudConfigKeysNotList(t *testing.T) {
|
||||||
|
contents := `
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- foo: bar
|
||||||
|
`
|
||||||
|
cfg, err := NewCloudConfig(contents)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encountered unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := cfg.SSHAuthorizedKeys
|
||||||
|
if len(keys) != 0 {
|
||||||
|
t.Error("Parsed incorrect number of SSH keys")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudConfigSerializationHeader(t *testing.T) {
|
||||||
|
cfg, _ := NewCloudConfig("")
|
||||||
|
contents := cfg.String()
|
||||||
|
header := strings.SplitN(contents, "\n", 2)[0]
|
||||||
|
if header != "#cloud-config" {
|
||||||
|
t.Fatalf("Serialized config did not have expected header")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudConfigUsers(t *testing.T) {
|
||||||
|
contents := `
|
||||||
|
users:
|
||||||
|
- name: elroy
|
||||||
|
passwd: somehash
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- somekey
|
||||||
|
gecos: arbitrary comment
|
||||||
|
homedir: /home/place
|
||||||
|
no_create_home: yes
|
||||||
|
primary_group: things
|
||||||
|
groups:
|
||||||
|
- ping
|
||||||
|
- pong
|
||||||
|
no_user_group: true
|
||||||
|
system: y
|
||||||
|
no_log_init: True
|
||||||
|
`
|
||||||
|
cfg, err := NewCloudConfig(contents)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encountered unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.Users) != 1 {
|
||||||
|
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
|
||||||
|
}
|
||||||
|
|
||||||
|
user := cfg.Users[0]
|
||||||
|
|
||||||
|
if user.Name != "elroy" {
|
||||||
|
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.PasswordHash != "somehash" {
|
||||||
|
t.Errorf("User passwd is %q, expected 'somehash'", user.PasswordHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys := user.SSHAuthorizedKeys; len(keys) != 1 {
|
||||||
|
t.Errorf("Parsed %d ssh keys, expected 1", len(keys))
|
||||||
|
} else {
|
||||||
|
key := user.SSHAuthorizedKeys[0]
|
||||||
|
if key != "somekey" {
|
||||||
|
t.Errorf("User SSH key is %q, expected 'somekey'", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.GECOS != "arbitrary comment" {
|
||||||
|
t.Errorf("Failed to parse gecos field, got %q", user.GECOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Homedir != "/home/place" {
|
||||||
|
t.Errorf("Failed to parse homedir field, got %q", user.Homedir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.NoCreateHome {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(user.Groups) != 2 {
|
||||||
|
t.Errorf("Failed to parse 2 goups, got %d", len(user.Groups))
|
||||||
|
} else {
|
||||||
|
if user.Groups[0] != "ping" {
|
||||||
|
t.Errorf("First group was %q, not expected value 'ping'", user.Groups[0])
|
||||||
|
}
|
||||||
|
if user.Groups[1] != "pong" {
|
||||||
|
t.Errorf("First group was %q, not expected value 'pong'", user.Groups[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.NoUserGroup {
|
||||||
|
t.Errorf("Failed to parse no_user_group field")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.System {
|
||||||
|
t.Errorf("Failed to parse system field")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.NoLogInit {
|
||||||
|
t.Errorf("Failed to parse no_log_init field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudConfigUsersGithubUser(t *testing.T) {
|
||||||
|
|
||||||
|
contents := `
|
||||||
|
users:
|
||||||
|
- name: elroy
|
||||||
|
coreos_ssh_import_github: bcwaldon
|
||||||
|
`
|
||||||
|
cfg, err := NewCloudConfig(contents)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encountered unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.Users) != 1 {
|
||||||
|
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
|
||||||
|
}
|
||||||
|
|
||||||
|
user := cfg.Users[0]
|
||||||
|
|
||||||
|
if user.Name != "elroy" {
|
||||||
|
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.SSHImportGithubUser != "bcwaldon" {
|
||||||
|
t.Errorf("github user is %q, expected 'bcwaldon'", user.SSHImportGithubUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
`
|
||||||
|
cfg, err := NewCloudConfig(contents)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encountered unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.Users) != 1 {
|
||||||
|
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
|
||||||
|
}
|
||||||
|
|
||||||
|
user := cfg.Users[0]
|
||||||
|
|
||||||
|
if user.Name != "elroy" {
|
||||||
|
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.SSHImportURL != "https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys" {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
config/etc_hosts.go
Normal file
19
config/etc_hosts.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 CoreOS, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
type EtcHosts string
|
53
config/etcd.go
Normal file
53
config/etcd.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 CoreOS, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
type Etcd struct {
|
||||||
|
Addr string `yaml:"addr" env:"ETCD_ADDR"`
|
||||||
|
BindAddr string `yaml:"bind_addr" env:"ETCD_BIND_ADDR"`
|
||||||
|
CAFile string `yaml:"ca_file" env:"ETCD_CA_FILE"`
|
||||||
|
CertFile string `yaml:"cert_file" env:"ETCD_CERT_FILE"`
|
||||||
|
ClusterActiveSize 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"`
|
||||||
|
}
|
25
config/file.go
Normal file
25
config/file.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 CoreOS, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Encoding string `yaml:"encoding" valid:"base64,b64,gz,gzip,gz+base64,gzip+base64,gz+b64,gzip+b64"`
|
||||||
|
Content string `yaml:"content"`
|
||||||
|
Owner string `yaml:"owner"`
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
RawFilePermissions string `yaml:"permissions"`
|
||||||
|
}
|
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"`
|
||||||
|
}
|
31
config/fleet.go
Normal file
31
config/fleet.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 CoreOS, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
type Fleet struct {
|
||||||
|
AgentTTL string `yaml:"agent_ttl" env:"FLEET_AGENT_TTL"`
|
||||||
|
EngineReconcileInterval 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"`
|
||||||
|
}
|
8
config/locksmith.go
Normal file
8
config/locksmith.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Locksmith struct {
|
||||||
|
Endpoint string `yaml:"endpoint" env:"LOCKSMITHD_ENDPOINT"`
|
||||||
|
EtcdCAFile string `yaml:"etcd_cafile" env:"LOCKSMITHD_ETCD_CAFILE"`
|
||||||
|
EtcdCertFile string `yaml:"etcd_certfile" env:"LOCKSMITHD_ETCD_CERTFILE"`
|
||||||
|
EtcdKeyFile string `yaml:"etcd_keyfile" env:"LOCKSMITHD_ETCD_KEYFILE"`
|
||||||
|
}
|
25
config/oem.go
Normal file
25
config/oem.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 CoreOS, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
type OEM struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
VersionID string `yaml:"version_id"`
|
||||||
|
HomeURL string `yaml:"home_url"`
|
||||||
|
BugReportURL string `yaml:"bug_report_url"`
|
||||||
|
}
|
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
|
||||||
|
}
|
32
config/unit.go
Normal file
32
config/unit.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
|
||||||
|
|
||||||
|
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"`
|
||||||
|
Content string `yaml:"content"`
|
||||||
|
}
|
23
config/update.go
Normal file
23
config/update.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 CoreOS, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
RebootStrategy string `yaml:"reboot_strategy" env:"REBOOT_STRATEGY" valid:"best-effort,etcd-lock,reboot,off,false"`
|
||||||
|
Group string `yaml:"group" env:"GROUP"`
|
||||||
|
Server string `yaml:"server" env:"SERVER"`
|
||||||
|
}
|
33
config/user.go
Normal file
33
config/user.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 CoreOS, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
PasswordHash string `yaml:"passwd"`
|
||||||
|
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
|
||||||
|
SSHImportGithubUser string `yaml:"coreos_ssh_import_github"`
|
||||||
|
SSHImportURL string `yaml:"coreos_ssh_import_url"`
|
||||||
|
GECOS string `yaml:"gecos"`
|
||||||
|
Homedir string `yaml:"homedir"`
|
||||||
|
NoCreateHome bool `yaml:"no_create_home"`
|
||||||
|
PrimaryGroup string `yaml:"primary_group"`
|
||||||
|
Groups []string `yaml:"groups"`
|
||||||
|
NoUserGroup bool `yaml:"no_user_group"`
|
||||||
|
System bool `yaml:"system"`
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
config/validate/validate.go
Normal file
116
config/validate/validate.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
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 len(userdataBytes) == 0:
|
||||||
|
return Report{}, nil
|
||||||
|
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
|
||||||
|
}
|
144
config/validate/validate_test.go
Normal file
144
config/validate/validate_test.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
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 TestValidate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
config string
|
||||||
|
|
||||||
|
report Report
|
||||||
|
}{
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
config: "#!/bin/bash\necho hey",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
r, err := Validate([]byte(tt.config))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("bad error (case #%d): want %v, got %v", i, nil, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(tt.report, r) {
|
||||||
|
t.Errorf("bad report (case #%d): want %+v, got %+v", i, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -23,6 +23,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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"
|
||||||
"github.com/coreos/coreos-cloudinit/datasource/configdrive"
|
"github.com/coreos/coreos-cloudinit/datasource/configdrive"
|
||||||
"github.com/coreos/coreos-cloudinit/datasource/file"
|
"github.com/coreos/coreos-cloudinit/datasource/file"
|
||||||
@@ -38,7 +40,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "0.10.9"
|
version = "1.0.2"
|
||||||
datasourceInterval = 100 * time.Millisecond
|
datasourceInterval = 100 * time.Millisecond
|
||||||
datasourceMaxInterval = 30 * time.Second
|
datasourceMaxInterval = 30 * time.Second
|
||||||
datasourceTimeout = 5 * time.Minute
|
datasourceTimeout = 5 * time.Minute
|
||||||
@@ -63,6 +65,7 @@ var (
|
|||||||
workspace string
|
workspace string
|
||||||
sshKeyName string
|
sshKeyName string
|
||||||
oem string
|
oem string
|
||||||
|
validate bool
|
||||||
}{}
|
}{}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,6 +85,7 @@ func init() {
|
|||||||
flag.StringVar(&flags.convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files")
|
flag.StringVar(&flags.convertNetconf, "convert-netconf", "", "Read the network config provided in cloud-drive and translate it from the specified format into networkd unit files")
|
||||||
flag.StringVar(&flags.workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
|
flag.StringVar(&flags.workspace, "workspace", "/var/lib/coreos-cloudinit", "Base directory coreos-cloudinit should use to store data")
|
||||||
flag.StringVar(&flags.sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
|
flag.StringVar(&flags.sshKeyName, "ssh-key-name", initialize.DefaultSSHKeyName, "Add SSH keys to the system with the given name")
|
||||||
|
flag.BoolVar(&flags.validate, "validate", false, "[EXPERIMENTAL] Validate the user-data but do not apply it to the system")
|
||||||
}
|
}
|
||||||
|
|
||||||
type oemConfig map[string]string
|
type oemConfig map[string]string
|
||||||
@@ -157,6 +161,22 @@ func main() {
|
|||||||
failure = true
|
failure = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if report, err := validate.Validate(userdataBytes); err == nil {
|
||||||
|
ret := 0
|
||||||
|
for _, e := range report.Entries() {
|
||||||
|
fmt.Println(e)
|
||||||
|
ret = 1
|
||||||
|
}
|
||||||
|
if flags.validate {
|
||||||
|
os.Exit(ret)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Failed while validating user_data (%q)\n", err)
|
||||||
|
if flags.validate {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("Fetching meta-data from datasource of type %q\n", ds.Type())
|
fmt.Printf("Fetching meta-data from datasource of type %q\n", ds.Type())
|
||||||
metadataBytes, err := ds.FetchMetadata()
|
metadataBytes, err := ds.FetchMetadata()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -178,8 +198,8 @@ func main() {
|
|||||||
env := initialize.NewEnvironment("/", ds.ConfigRoot(), flags.workspace, flags.convertNetconf, flags.sshKeyName, subs)
|
env := initialize.NewEnvironment("/", ds.ConfigRoot(), flags.workspace, flags.convertNetconf, flags.sshKeyName, subs)
|
||||||
userdata := env.Apply(string(userdataBytes))
|
userdata := env.Apply(string(userdataBytes))
|
||||||
|
|
||||||
var ccm, ccu *initialize.CloudConfig
|
var ccm, ccu *config.CloudConfig
|
||||||
var script *system.Script
|
var script *config.Script
|
||||||
if ccm, err = initialize.ParseMetaData(string(metadataBytes)); err != nil {
|
if ccm, err = initialize.ParseMetaData(string(metadataBytes)); err != nil {
|
||||||
fmt.Printf("Failed to parse meta-data: %v\n", err)
|
fmt.Printf("Failed to parse meta-data: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -200,14 +220,14 @@ func main() {
|
|||||||
failure = true
|
failure = true
|
||||||
} else {
|
} else {
|
||||||
switch t := ud.(type) {
|
switch t := ud.(type) {
|
||||||
case *initialize.CloudConfig:
|
case *config.CloudConfig:
|
||||||
ccu = t
|
ccu = t
|
||||||
case system.Script:
|
case config.Script:
|
||||||
script = &t
|
script = &t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cc *initialize.CloudConfig
|
var cc *config.CloudConfig
|
||||||
if ccm != nil && ccu != nil {
|
if ccm != nil && ccu != nil {
|
||||||
fmt.Println("Merging cloud-config from meta-data and user-data")
|
fmt.Println("Merging cloud-config from meta-data and user-data")
|
||||||
merged := mergeCloudConfig(*ccm, *ccu)
|
merged := mergeCloudConfig(*ccm, *ccu)
|
||||||
@@ -246,7 +266,7 @@ func main() {
|
|||||||
// not already set on udcc (i.e. user-data always takes precedence)
|
// not already set on udcc (i.e. user-data always takes precedence)
|
||||||
// NB: This needs to be kept in sync with ParseMetadata so that it tracks all
|
// NB: This needs to be kept in sync with ParseMetadata so that it tracks all
|
||||||
// elements of a CloudConfig which that function can populate.
|
// elements of a CloudConfig which that function can populate.
|
||||||
func mergeCloudConfig(mdcc, udcc initialize.CloudConfig) (cc initialize.CloudConfig) {
|
func mergeCloudConfig(mdcc, udcc config.CloudConfig) (cc config.CloudConfig) {
|
||||||
if mdcc.Hostname != "" {
|
if mdcc.Hostname != "" {
|
||||||
if udcc.Hostname != "" {
|
if udcc.Hostname != "" {
|
||||||
fmt.Printf("Warning: user-data hostname (%s) overrides metadata hostname (%s)\n", udcc.Hostname, mdcc.Hostname)
|
fmt.Printf("Warning: user-data hostname (%s) overrides metadata hostname (%s)\n", udcc.Hostname, mdcc.Hostname)
|
||||||
@@ -361,7 +381,7 @@ func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(jonboulle): this should probably be refactored and moved into a different module
|
// TODO(jonboulle): this should probably be refactored and moved into a different module
|
||||||
func runScript(script system.Script, env *initialize.Environment) error {
|
func runScript(script config.Script, env *initialize.Environment) error {
|
||||||
err := initialize.PrepWorkspace(env.Workspace())
|
err := initialize.PrepWorkspace(env.Workspace())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed preparing workspace: %v\n", err)
|
fmt.Printf("Failed preparing workspace: %v\n", err)
|
||||||
|
@@ -20,37 +20,37 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/initialize"
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMergeCloudConfig(t *testing.T) {
|
func TestMergeCloudConfig(t *testing.T) {
|
||||||
simplecc := initialize.CloudConfig{
|
simplecc := config.CloudConfig{
|
||||||
SSHAuthorizedKeys: []string{"abc", "def"},
|
SSHAuthorizedKeys: []string{"abc", "def"},
|
||||||
Hostname: "foobar",
|
Hostname: "foobar",
|
||||||
NetworkConfigPath: "/path/somewhere",
|
NetworkConfigPath: "/path/somewhere",
|
||||||
NetworkConfig: `{}`,
|
NetworkConfig: `{}`,
|
||||||
}
|
}
|
||||||
for i, tt := range []struct {
|
for i, tt := range []struct {
|
||||||
udcc initialize.CloudConfig
|
udcc config.CloudConfig
|
||||||
mdcc initialize.CloudConfig
|
mdcc config.CloudConfig
|
||||||
want initialize.CloudConfig
|
want config.CloudConfig
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
// If mdcc is empty, udcc should be returned unchanged
|
// If mdcc is empty, udcc should be returned unchanged
|
||||||
simplecc,
|
simplecc,
|
||||||
initialize.CloudConfig{},
|
config.CloudConfig{},
|
||||||
simplecc,
|
simplecc,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// If udcc is empty, mdcc should be returned unchanged(overridden)
|
// If udcc is empty, mdcc should be returned unchanged(overridden)
|
||||||
initialize.CloudConfig{},
|
config.CloudConfig{},
|
||||||
simplecc,
|
simplecc,
|
||||||
simplecc,
|
simplecc,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// user-data should override completely in the case of conflicts
|
// user-data should override completely in the case of conflicts
|
||||||
simplecc,
|
simplecc,
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
Hostname: "meta-hostname",
|
Hostname: "meta-hostname",
|
||||||
NetworkConfigPath: "/path/meta",
|
NetworkConfigPath: "/path/meta",
|
||||||
NetworkConfig: `{"hostname":"test"}`,
|
NetworkConfig: `{"hostname":"test"}`,
|
||||||
@@ -59,17 +59,17 @@ func TestMergeCloudConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Mixed merge should succeed
|
// Mixed merge should succeed
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
SSHAuthorizedKeys: []string{"abc", "def"},
|
SSHAuthorizedKeys: []string{"abc", "def"},
|
||||||
Hostname: "user-hostname",
|
Hostname: "user-hostname",
|
||||||
NetworkConfigPath: "/path/somewhere",
|
NetworkConfigPath: "/path/somewhere",
|
||||||
NetworkConfig: `{"hostname":"test"}`,
|
NetworkConfig: `{"hostname":"test"}`,
|
||||||
},
|
},
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
SSHAuthorizedKeys: []string{"woof", "qux"},
|
SSHAuthorizedKeys: []string{"woof", "qux"},
|
||||||
Hostname: "meta-hostname",
|
Hostname: "meta-hostname",
|
||||||
},
|
},
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
SSHAuthorizedKeys: []string{"abc", "def", "woof", "qux"},
|
SSHAuthorizedKeys: []string{"abc", "def", "woof", "qux"},
|
||||||
Hostname: "user-hostname",
|
Hostname: "user-hostname",
|
||||||
NetworkConfigPath: "/path/somewhere",
|
NetworkConfigPath: "/path/somewhere",
|
||||||
@@ -78,15 +78,15 @@ func TestMergeCloudConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Completely non-conflicting merge should be fine
|
// Completely non-conflicting merge should be fine
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
Hostname: "supercool",
|
Hostname: "supercool",
|
||||||
},
|
},
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"},
|
SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"},
|
||||||
NetworkConfigPath: "/dev/fun",
|
NetworkConfigPath: "/dev/fun",
|
||||||
NetworkConfig: `{"hostname":"test"}`,
|
NetworkConfig: `{"hostname":"test"}`,
|
||||||
},
|
},
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
Hostname: "supercool",
|
Hostname: "supercool",
|
||||||
SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"},
|
SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"},
|
||||||
NetworkConfigPath: "/dev/fun",
|
NetworkConfigPath: "/dev/fun",
|
||||||
@@ -95,33 +95,33 @@ func TestMergeCloudConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Non-mergeable settings in user-data should not be affected
|
// Non-mergeable settings in user-data should not be affected
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
Hostname: "mememe",
|
Hostname: "mememe",
|
||||||
ManageEtcHosts: initialize.EtcHosts("lolz"),
|
ManageEtcHosts: config.EtcHosts("lolz"),
|
||||||
},
|
},
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
Hostname: "youyouyou",
|
Hostname: "youyouyou",
|
||||||
NetworkConfigPath: "meta-meta-yo",
|
NetworkConfigPath: "meta-meta-yo",
|
||||||
NetworkConfig: `{"hostname":"test"}`,
|
NetworkConfig: `{"hostname":"test"}`,
|
||||||
},
|
},
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
Hostname: "mememe",
|
Hostname: "mememe",
|
||||||
ManageEtcHosts: initialize.EtcHosts("lolz"),
|
ManageEtcHosts: config.EtcHosts("lolz"),
|
||||||
NetworkConfigPath: "meta-meta-yo",
|
NetworkConfigPath: "meta-meta-yo",
|
||||||
NetworkConfig: `{"hostname":"test"}`,
|
NetworkConfig: `{"hostname":"test"}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Non-mergeable (unexpected) settings in meta-data are ignored
|
// Non-mergeable (unexpected) settings in meta-data are ignored
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
Hostname: "mememe",
|
Hostname: "mememe",
|
||||||
},
|
},
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
ManageEtcHosts: initialize.EtcHosts("lolz"),
|
ManageEtcHosts: config.EtcHosts("lolz"),
|
||||||
NetworkConfigPath: "meta-meta-yo",
|
NetworkConfigPath: "meta-meta-yo",
|
||||||
NetworkConfig: `{"hostname":"test"}`,
|
NetworkConfig: `{"hostname":"test"}`,
|
||||||
},
|
},
|
||||||
initialize.CloudConfig{
|
config.CloudConfig{
|
||||||
Hostname: "mememe",
|
Hostname: "mememe",
|
||||||
NetworkConfigPath: "meta-meta-yo",
|
NetworkConfigPath: "meta-meta-yo",
|
||||||
NetworkConfig: `{"hostname":"test"}`,
|
NetworkConfig: `{"hostname":"test"}`,
|
||||||
|
@@ -17,8 +17,12 @@
|
|||||||
package cloudsigma
|
package cloudsigma
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -51,7 +55,8 @@ func (_ *serverContextService) IsAvailable() bool {
|
|||||||
}
|
}
|
||||||
productName := make([]byte, 10)
|
productName := make([]byte, 10)
|
||||||
_, err = productNameFile.Read(productName)
|
_, err = productNameFile.Read(productName)
|
||||||
return err == nil && string(productName) == "CloudSigma"
|
|
||||||
|
return err == nil && string(productName) == "CloudSigma" && hasDHCPLeases()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_ *serverContextService) AvailabilityChanges() bool {
|
func (_ *serverContextService) AvailabilityChanges() bool {
|
||||||
@@ -73,12 +78,16 @@ func (scs *serverContextService) FetchMetadata() ([]byte, error) {
|
|||||||
UUID string `json:"uuid"`
|
UUID string `json:"uuid"`
|
||||||
Meta map[string]string `json:"meta"`
|
Meta map[string]string `json:"meta"`
|
||||||
Nics []struct {
|
Nics []struct {
|
||||||
Runtime struct {
|
Mac string `json:"mac"`
|
||||||
|
IPv4Conf struct {
|
||||||
InterfaceType string `json:"interface_type"`
|
InterfaceType string `json:"interface_type"`
|
||||||
IPv4 struct {
|
IP struct {
|
||||||
IP string `json:"uuid"`
|
UUID string `json:"uuid"`
|
||||||
} `json:"ip_v4"`
|
} `json:"ip"`
|
||||||
} `json:"runtime"`
|
} `json:"ip_v4_conf"`
|
||||||
|
VLAN struct {
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
} `json:"vlan"`
|
||||||
} `json:"nics"`
|
} `json:"nics"`
|
||||||
}
|
}
|
||||||
outputMetadata struct {
|
outputMetadata struct {
|
||||||
@@ -112,11 +121,12 @@ func (scs *serverContextService) FetchMetadata() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, nic := range inputMetadata.Nics {
|
for _, nic := range inputMetadata.Nics {
|
||||||
if nic.Runtime.IPv4.IP != "" {
|
if nic.IPv4Conf.IP.UUID != "" {
|
||||||
if nic.Runtime.InterfaceType == "public" {
|
outputMetadata.PublicIPv4 = nic.IPv4Conf.IP.UUID
|
||||||
outputMetadata.PublicIPv4 = nic.Runtime.IPv4.IP
|
}
|
||||||
} else {
|
if nic.VLAN.UUID != "" {
|
||||||
outputMetadata.LocalIPv4 = nic.Runtime.IPv4.IP
|
if localIP, err := scs.findLocalIP(nic.Mac); err == nil {
|
||||||
|
outputMetadata.LocalIPv4 = localIP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,6 +156,36 @@ func (scs *serverContextService) FetchNetworkConfig(a string) ([]byte, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (scs *serverContextService) findLocalIP(mac string) (string, error) {
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
ifaceMac, err := net.ParseMAC(mac)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
if !bytes.Equal(iface.HardwareAddr, ifaceMac) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
switch ip := addr.(type) {
|
||||||
|
case *net.IPNet:
|
||||||
|
if ip.IP.To4() != nil {
|
||||||
|
return ip.IP.To4().String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("Local IP not found")
|
||||||
|
}
|
||||||
|
|
||||||
func isBase64Encoded(field string, userdata map[string]string) bool {
|
func isBase64Encoded(field string, userdata map[string]string) bool {
|
||||||
base64Fields, ok := userdata["base64_fields"]
|
base64Fields, ok := userdata["base64_fields"]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -159,3 +199,8 @@ func isBase64Encoded(field string, userdata map[string]string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasDHCPLeases() bool {
|
||||||
|
files, err := ioutil.ReadDir("/run/systemd/netif/leases/")
|
||||||
|
return err == nil && len(files) > 0
|
||||||
|
}
|
||||||
|
@@ -74,14 +74,41 @@ func TestServerContextFetchMetadata(t *testing.T) {
|
|||||||
"name": "coreos",
|
"name": "coreos",
|
||||||
"nics": [
|
"nics": [
|
||||||
{
|
{
|
||||||
"runtime": {
|
"boot_order": null,
|
||||||
"interface_type": "public",
|
"ip_v4_conf": {
|
||||||
"ip_v4": {
|
"conf": "dhcp",
|
||||||
|
"ip": {
|
||||||
|
"gateway": "31.171.244.1",
|
||||||
|
"meta": {},
|
||||||
|
"nameservers": [
|
||||||
|
"178.22.66.167",
|
||||||
|
"178.22.71.56",
|
||||||
|
"8.8.8.8"
|
||||||
|
],
|
||||||
|
"netmask": 22,
|
||||||
|
"tags": [],
|
||||||
"uuid": "31.171.251.74"
|
"uuid": "31.171.251.74"
|
||||||
},
|
}
|
||||||
"ip_v6": null
|
|
||||||
},
|
},
|
||||||
|
"ip_v6_conf": null,
|
||||||
|
"mac": "22:3d:09:6b:90:f3",
|
||||||
|
"model": "virtio",
|
||||||
"vlan": null
|
"vlan": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"boot_order": null,
|
||||||
|
"ip_v4_conf": null,
|
||||||
|
"ip_v6_conf": null,
|
||||||
|
"mac": "22:ae:4a:fb:8f:31",
|
||||||
|
"model": "virtio",
|
||||||
|
"vlan": {
|
||||||
|
"meta": {
|
||||||
|
"description": "",
|
||||||
|
"name": "CoreOS"
|
||||||
|
},
|
||||||
|
"tags": [],
|
||||||
|
"uuid": "5dec030e-25b8-4621-a5a4-a3302c9d9619"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"smp": 2,
|
"smp": 2,
|
||||||
@@ -106,12 +133,8 @@ func TestServerContextFetchMetadata(t *testing.T) {
|
|||||||
t.Error("Public SSH Keys are not being read properly")
|
t.Error("Public SSH Keys are not being read properly")
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata.LocalIPv4 != "" {
|
|
||||||
t.Errorf("Local IP is not empty but %s instead", metadata.LocalIPv4)
|
|
||||||
}
|
|
||||||
|
|
||||||
if metadata.PublicIPv4 != "31.171.251.74" {
|
if metadata.PublicIPv4 != "31.171.251.74" {
|
||||||
t.Errorf("Local IP is not 31.171.251.74 but %s instead", metadata.PublicIPv4)
|
t.Errorf("Public IP is not 31.171.251.74 but %s instead", metadata.PublicIPv4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,8 +22,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/gopkg.in/yaml.v1"
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/network"
|
"github.com/coreos/coreos-cloudinit/network"
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
@@ -33,146 +32,19 @@ import (
|
|||||||
type CloudConfigFile interface {
|
type CloudConfigFile interface {
|
||||||
// File should either return (*system.File, error), or (nil, nil) if nothing
|
// File should either return (*system.File, error), or (nil, nil) if nothing
|
||||||
// needs to be done for this configuration option.
|
// needs to be done for this configuration option.
|
||||||
File(root string) (*system.File, error)
|
File() (*system.File, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloudConfigUnit represents a CoreOS specific configuration option that can generate
|
// CloudConfigUnit represents a CoreOS specific configuration option that can generate
|
||||||
// associated system.Units to be created/enabled appropriately
|
// associated system.Units to be created/enabled appropriately
|
||||||
type CloudConfigUnit interface {
|
type CloudConfigUnit interface {
|
||||||
Units(root string) ([]system.Unit, error)
|
Units() []system.Unit
|
||||||
}
|
|
||||||
|
|
||||||
// CloudConfig encapsulates the entire cloud-config configuration file and maps directly to YAML
|
|
||||||
type CloudConfig struct {
|
|
||||||
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
|
|
||||||
Coreos struct {
|
|
||||||
Etcd EtcdEnvironment
|
|
||||||
Fleet FleetEnvironment
|
|
||||||
OEM OEMRelease
|
|
||||||
Update UpdateConfig
|
|
||||||
Units []system.Unit
|
|
||||||
}
|
|
||||||
WriteFiles []system.File `yaml:"write_files"`
|
|
||||||
Hostname string
|
|
||||||
Users []system.User
|
|
||||||
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
|
|
||||||
NetworkConfigPath string
|
|
||||||
NetworkConfig string
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(&system.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(&system.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
if err != nil {
|
|
||||||
return &cfg, err
|
|
||||||
}
|
|
||||||
warnOnUnrecognizedKeys(contents, log.Printf)
|
|
||||||
return &cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cc CloudConfig) String() string {
|
|
||||||
bytes, err := yaml.Marshal(cc)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
stringified := string(bytes)
|
|
||||||
stringified = fmt.Sprintf("#cloud-config\n%s", stringified)
|
|
||||||
|
|
||||||
return stringified
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply renders a CloudConfig to an Environment. This can involve things like
|
// Apply renders a CloudConfig to an Environment. This can involve things like
|
||||||
// configuring the hostname, adding new users, writing various configuration
|
// configuring the hostname, adding new users, writing various configuration
|
||||||
// files to disk, and manipulating systemd services.
|
// files to disk, and manipulating systemd services.
|
||||||
func Apply(cfg CloudConfig, env *Environment) error {
|
func Apply(cfg config.CloudConfig, env *Environment) error {
|
||||||
if cfg.Hostname != "" {
|
if cfg.Hostname != "" {
|
||||||
if err := system.SetHostname(cfg.Hostname); err != nil {
|
if err := system.SetHostname(cfg.Hostname); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -232,26 +104,42 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ccf := range []CloudConfigFile{cfg.Coreos.OEM, cfg.Coreos.Update, cfg.ManageEtcHosts} {
|
var writeFiles []system.File
|
||||||
f, err := ccf.File(env.Root())
|
for _, file := range cfg.WriteFiles {
|
||||||
|
writeFiles = append(writeFiles, system.File{File: file})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ccf := range []CloudConfigFile{
|
||||||
|
system.OEM{OEM: cfg.Coreos.OEM},
|
||||||
|
system.Update{Update: cfg.Coreos.Update, ReadConfig: system.DefaultReadConfig},
|
||||||
|
system.EtcHosts{EtcHosts: cfg.ManageEtcHosts},
|
||||||
|
} {
|
||||||
|
f, err := ccf.File()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if f != nil {
|
if f != nil {
|
||||||
cfg.WriteFiles = append(cfg.WriteFiles, *f)
|
writeFiles = append(writeFiles, *f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ccu := range []CloudConfigUnit{cfg.Coreos.Etcd, cfg.Coreos.Fleet, cfg.Coreos.Update} {
|
var units []system.Unit
|
||||||
u, err := ccu.Units(env.Root())
|
for _, u := range cfg.Coreos.Units {
|
||||||
if err != nil {
|
units = append(units, system.Unit{Unit: u})
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
cfg.Coreos.Units = append(cfg.Coreos.Units, u...)
|
for _, ccu := range []CloudConfigUnit{
|
||||||
|
system.Etcd{Etcd: cfg.Coreos.Etcd},
|
||||||
|
system.Fleet{Fleet: cfg.Coreos.Fleet},
|
||||||
|
system.Locksmith{Locksmith: cfg.Coreos.Locksmith},
|
||||||
|
system.Flannel{Flannel: cfg.Coreos.Flannel},
|
||||||
|
system.Update{Update: cfg.Coreos.Update, ReadConfig: system.DefaultReadConfig},
|
||||||
|
} {
|
||||||
|
units = append(units, ccu.Units()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
wroteEnvironment := false
|
wroteEnvironment := false
|
||||||
for _, file := range cfg.WriteFiles {
|
for _, file := range writeFiles {
|
||||||
fullPath, err := system.WriteFile(&file, env.Root())
|
fullPath, err := system.WriteFile(&file, env.Root())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -282,24 +170,39 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
|||||||
case "digitalocean":
|
case "digitalocean":
|
||||||
interfaces, err = network.ProcessDigitalOceanNetconf(cfg.NetworkConfig)
|
interfaces, err = network.ProcessDigitalOceanNetconf(cfg.NetworkConfig)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Unsupported network config format %q", env.NetconfType())
|
err = fmt.Errorf("Unsupported network config format %q", env.NetconfType())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := system.WriteNetworkdConfigs(interfaces); err != nil {
|
units = append(units, createNetworkingUnits(interfaces)...)
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := system.RestartNetwork(interfaces); err != nil {
|
if err := system.RestartNetwork(interfaces); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
um := system.NewUnitManager(env.Root())
|
um := system.NewUnitManager(env.Root())
|
||||||
return processUnits(cfg.Coreos.Units, env.Root(), um)
|
return processUnits(units, env.Root(), um)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNetworkingUnits(interfaces []network.InterfaceGenerator) (units []system.Unit) {
|
||||||
|
appendNewUnit := func(units []system.Unit, name, content string) []system.Unit {
|
||||||
|
if content == "" {
|
||||||
|
return units
|
||||||
|
}
|
||||||
|
return append(units, system.Unit{Unit: config.Unit{
|
||||||
|
Name: name,
|
||||||
|
Runtime: true,
|
||||||
|
Content: content,
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
for _, i := range interfaces {
|
||||||
|
units = appendNewUnit(units, fmt.Sprintf("%s.netdev", i.Filename()), i.Netdev())
|
||||||
|
units = appendNewUnit(units, fmt.Sprintf("%s.link", i.Filename()), i.Link())
|
||||||
|
units = appendNewUnit(units, fmt.Sprintf("%s.network", i.Filename()), i.Network())
|
||||||
|
}
|
||||||
|
return units
|
||||||
}
|
}
|
||||||
|
|
||||||
// processUnits takes a set of Units and applies them to the given root using
|
// processUnits takes a set of Units and applies them to the given root using
|
||||||
@@ -308,63 +211,79 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
|||||||
// commands against units. It returns any error encountered.
|
// commands against units. It returns any error encountered.
|
||||||
func processUnits(units []system.Unit, root string, um system.UnitManager) error {
|
func processUnits(units []system.Unit, root string, um system.UnitManager) error {
|
||||||
type action struct {
|
type action struct {
|
||||||
unit string
|
unit system.Unit
|
||||||
command string
|
command string
|
||||||
}
|
}
|
||||||
actions := make([]action, 0, len(units))
|
actions := make([]action, 0, len(units))
|
||||||
reload := false
|
reload := false
|
||||||
restartNetworkd := false
|
restartNetworkd := false
|
||||||
for _, unit := range units {
|
for _, unit := range units {
|
||||||
dst := unit.Destination(root)
|
if unit.Name == "" {
|
||||||
|
log.Printf("Skipping unit without name")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if unit.Content != "" {
|
if unit.Content != "" {
|
||||||
log.Printf("Writing unit %s to filesystem at path %s", unit.Name, dst)
|
log.Printf("Writing unit %q to filesystem", unit.Name)
|
||||||
if err := um.PlaceUnit(&unit, dst); err != nil {
|
if err := um.PlaceUnit(unit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("Placed unit %s at %s", unit.Name, dst)
|
log.Printf("Wrote unit %q", unit.Name)
|
||||||
reload = true
|
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 {
|
if unit.Mask {
|
||||||
log.Printf("Masking unit file %s", unit.Name)
|
log.Printf("Masking unit file %q", unit.Name)
|
||||||
if err := um.MaskUnit(&unit); err != nil {
|
if err := um.MaskUnit(unit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if unit.Runtime {
|
} else if unit.Runtime {
|
||||||
log.Printf("Ensuring runtime unit file %s is unmasked", unit.Name)
|
log.Printf("Ensuring runtime unit file %q is unmasked", unit.Name)
|
||||||
if err := um.UnmaskUnit(&unit); err != nil {
|
if err := um.UnmaskUnit(unit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if unit.Enable {
|
if unit.Enable {
|
||||||
if unit.Group() != "network" {
|
if unit.Group() != "network" {
|
||||||
log.Printf("Enabling unit file %s", unit.Name)
|
log.Printf("Enabling unit file %q", unit.Name)
|
||||||
if err := um.EnableUnitFile(unit.Name, unit.Runtime); err != nil {
|
if err := um.EnableUnitFile(unit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("Enabled unit %s", unit.Name)
|
log.Printf("Enabled unit %q", unit.Name)
|
||||||
} else {
|
} 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" {
|
if unit.Group() == "network" {
|
||||||
restartNetworkd = true
|
restartNetworkd = true
|
||||||
} else if unit.Command != "" {
|
} else if unit.Command != "" {
|
||||||
actions = append(actions, action{unit.Name, unit.Command})
|
actions = append(actions, action{unit, unit.Command})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if reload {
|
if reload {
|
||||||
if err := um.DaemonReload(); err != nil {
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if restartNetworkd {
|
if restartNetworkd {
|
||||||
log.Printf("Restarting systemd-networkd")
|
log.Printf("Restarting systemd-networkd")
|
||||||
res, err := um.RunUnitCommand("restart", "systemd-networkd.service")
|
networkd := system.Unit{Unit: config.Unit{Name: "systemd-networkd.service"}}
|
||||||
|
res, err := um.RunUnitCommand(networkd, "restart")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -372,12 +291,12 @@ func processUnits(units []system.Unit, root string, um system.UnitManager) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, action := range actions {
|
for _, action := range actions {
|
||||||
log.Printf("Calling unit command '%s %s'", action.command, action.unit)
|
log.Printf("Calling unit command %q on %q'", action.command, action.unit.Name)
|
||||||
res, err := um.RunUnitCommand(action.command, action.unit)
|
res, err := um.RunUnitCommand(action.unit, action.command)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
|
@@ -17,368 +17,14 @@
|
|||||||
package initialize
|
package initialize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"reflect"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
"github.com/coreos/coreos-cloudinit/network"
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCloudConfigInvalidKeys(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
t.Fatalf("panic while instantiating CloudConfig with nil keys: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for _, tt := range []struct {
|
|
||||||
contents string
|
|
||||||
}{
|
|
||||||
{"coreos:"},
|
|
||||||
{"ssh_authorized_keys:"},
|
|
||||||
{"ssh_authorized_keys:\n -"},
|
|
||||||
{"ssh_authorized_keys:\n - 0:"},
|
|
||||||
{"write_files:"},
|
|
||||||
{"write_files:\n -"},
|
|
||||||
{"write_files:\n - 0:"},
|
|
||||||
{"users:"},
|
|
||||||
{"users:\n -"},
|
|
||||||
{"users:\n - 0:"},
|
|
||||||
} {
|
|
||||||
_, err := NewCloudConfig(tt.contents)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error instantiating CloudConfig with invalid keys: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCloudConfigUnknownKeys(t *testing.T) {
|
|
||||||
contents := `
|
|
||||||
coreos:
|
|
||||||
etcd:
|
|
||||||
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
|
|
||||||
coreos_unknown:
|
|
||||||
foo: "bar"
|
|
||||||
section_unknown:
|
|
||||||
dunno:
|
|
||||||
something
|
|
||||||
bare_unknown:
|
|
||||||
bar
|
|
||||||
write_files:
|
|
||||||
- content: fun
|
|
||||||
path: /var/party
|
|
||||||
file_unknown: nofun
|
|
||||||
users:
|
|
||||||
- name: fry
|
|
||||||
passwd: somehash
|
|
||||||
user_unknown: philip
|
|
||||||
hostname:
|
|
||||||
foo
|
|
||||||
`
|
|
||||||
cfg, err := NewCloudConfig(contents)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error instantiating CloudConfig with unknown keys: %v", err)
|
|
||||||
}
|
|
||||||
if cfg.Hostname != "foo" {
|
|
||||||
t.Fatalf("hostname not correctly set when invalid keys are present")
|
|
||||||
}
|
|
||||||
if len(cfg.Coreos.Etcd) < 1 {
|
|
||||||
t.Fatalf("etcd section not correctly set when invalid keys are present")
|
|
||||||
}
|
|
||||||
if len(cfg.WriteFiles) < 1 || cfg.WriteFiles[0].Content != "fun" || cfg.WriteFiles[0].Path != "/var/party" {
|
|
||||||
t.Fatalf("write_files section not correctly set when invalid keys are present")
|
|
||||||
}
|
|
||||||
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"
|
|
||||||
func TestCloudConfigEmpty(t *testing.T) {
|
|
||||||
cfg, err := NewCloudConfig("")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Encountered unexpected error :%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := cfg.SSHAuthorizedKeys
|
|
||||||
if len(keys) != 0 {
|
|
||||||
t.Error("Parsed incorrect number of SSH keys")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.WriteFiles) != 0 {
|
|
||||||
t.Error("Expected zero WriteFiles")
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Hostname != "" {
|
|
||||||
t.Errorf("Expected hostname to be empty, got '%s'", cfg.Hostname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that the parsing of a cloud config file "generally works"
|
|
||||||
func TestCloudConfig(t *testing.T) {
|
|
||||||
contents := `
|
|
||||||
coreos:
|
|
||||||
etcd:
|
|
||||||
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
|
|
||||||
update:
|
|
||||||
reboot-strategy: reboot
|
|
||||||
units:
|
|
||||||
- name: 50-eth0.network
|
|
||||||
runtime: yes
|
|
||||||
content: '[Match]
|
|
||||||
|
|
||||||
Name=eth47
|
|
||||||
|
|
||||||
|
|
||||||
[Network]
|
|
||||||
|
|
||||||
Address=10.209.171.177/19
|
|
||||||
|
|
||||||
'
|
|
||||||
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
|
|
||||||
ssh_authorized_keys:
|
|
||||||
- foobar
|
|
||||||
- foobaz
|
|
||||||
write_files:
|
|
||||||
- content: |
|
|
||||||
penny
|
|
||||||
elroy
|
|
||||||
path: /etc/dogepack.conf
|
|
||||||
permissions: '0644'
|
|
||||||
owner: root:dogepack
|
|
||||||
hostname: trontastic
|
|
||||||
`
|
|
||||||
cfg, err := NewCloudConfig(contents)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Encountered unexpected error :%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := cfg.SSHAuthorizedKeys
|
|
||||||
if len(keys) != 2 {
|
|
||||||
t.Error("Parsed incorrect number of SSH keys")
|
|
||||||
} else if keys[0] != "foobar" {
|
|
||||||
t.Error("Expected first SSH key to be 'foobar'")
|
|
||||||
} else if keys[1] != "foobaz" {
|
|
||||||
t.Error("Expected first SSH key to be 'foobaz'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.WriteFiles) != 1 {
|
|
||||||
t.Error("Failed to parse correct number of write_files")
|
|
||||||
} else {
|
|
||||||
wf := cfg.WriteFiles[0]
|
|
||||||
if wf.Content != "penny\nelroy\n" {
|
|
||||||
t.Errorf("WriteFile has incorrect contents '%s'", wf.Content)
|
|
||||||
}
|
|
||||||
if wf.Encoding != "" {
|
|
||||||
t.Errorf("WriteFile has incorrect encoding %s", wf.Encoding)
|
|
||||||
}
|
|
||||||
if perm, _ := wf.Permissions(); perm != 0644 {
|
|
||||||
t.Errorf("WriteFile has incorrect permissions %s", perm)
|
|
||||||
}
|
|
||||||
if wf.Path != "/etc/dogepack.conf" {
|
|
||||||
t.Errorf("WriteFile has incorrect path %s", wf.Path)
|
|
||||||
}
|
|
||||||
if wf.Owner != "root:dogepack" {
|
|
||||||
t.Errorf("WriteFile has incorrect owner %s", wf.Owner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.Coreos.Units) != 1 {
|
|
||||||
t.Error("Failed to parse correct number of units")
|
|
||||||
} else {
|
|
||||||
u := cfg.Coreos.Units[0]
|
|
||||||
expect := `[Match]
|
|
||||||
Name=eth47
|
|
||||||
|
|
||||||
[Network]
|
|
||||||
Address=10.209.171.177/19
|
|
||||||
`
|
|
||||||
if u.Content != expect {
|
|
||||||
t.Errorf("Unit has incorrect contents '%s'.\nExpected '%s'.", u.Content, expect)
|
|
||||||
}
|
|
||||||
if u.Runtime != true {
|
|
||||||
t.Errorf("Unit has incorrect runtime value")
|
|
||||||
}
|
|
||||||
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" {
|
|
||||||
t.Errorf("Failed parsing coreos.oem. Expected ID 'rackspace', got %q.", cfg.Coreos.OEM.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Hostname != "trontastic" {
|
|
||||||
t.Errorf("Failed to parse hostname")
|
|
||||||
}
|
|
||||||
if cfg.Coreos.Update["reboot-strategy"] != "reboot" {
|
|
||||||
t.Errorf("Failed to parse locksmith strategy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that our interface conversion doesn't panic
|
|
||||||
func TestCloudConfigKeysNotList(t *testing.T) {
|
|
||||||
contents := `
|
|
||||||
ssh_authorized_keys:
|
|
||||||
- foo: bar
|
|
||||||
`
|
|
||||||
cfg, err := NewCloudConfig(contents)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Encountered unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := cfg.SSHAuthorizedKeys
|
|
||||||
if len(keys) != 0 {
|
|
||||||
t.Error("Parsed incorrect number of SSH keys")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCloudConfigSerializationHeader(t *testing.T) {
|
|
||||||
cfg, _ := NewCloudConfig("")
|
|
||||||
contents := cfg.String()
|
|
||||||
header := strings.SplitN(contents, "\n", 2)[0]
|
|
||||||
if header != "#cloud-config" {
|
|
||||||
t.Fatalf("Serialized config did not have expected header")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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:
|
|
||||||
- somekey
|
|
||||||
gecos: arbitrary comment
|
|
||||||
homedir: /home/place
|
|
||||||
no-create-home: yes
|
|
||||||
primary-group: things
|
|
||||||
groups:
|
|
||||||
- ping
|
|
||||||
- pong
|
|
||||||
no-user-group: true
|
|
||||||
system: y
|
|
||||||
no-log-init: True
|
|
||||||
`
|
|
||||||
cfg, err := NewCloudConfig(contents)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Encountered unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.Users) != 1 {
|
|
||||||
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
|
|
||||||
}
|
|
||||||
|
|
||||||
user := cfg.Users[0]
|
|
||||||
|
|
||||||
if user.Name != "elroy" {
|
|
||||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.PasswordHash != "somehash" {
|
|
||||||
t.Errorf("User passwd is %q, expected 'somehash'", user.PasswordHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
if keys := user.SSHAuthorizedKeys; len(keys) != 1 {
|
|
||||||
t.Errorf("Parsed %d ssh keys, expected 1", len(keys))
|
|
||||||
} else {
|
|
||||||
key := user.SSHAuthorizedKeys[0]
|
|
||||||
if key != "somekey" {
|
|
||||||
t.Errorf("User SSH key is %q, expected 'somekey'", key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.GECOS != "arbitrary comment" {
|
|
||||||
t.Errorf("Failed to parse gecos field, got %q", user.GECOS)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.Homedir != "/home/place" {
|
|
||||||
t.Errorf("Failed to parse homedir field, got %q", user.Homedir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !user.NoCreateHome {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(user.Groups) != 2 {
|
|
||||||
t.Errorf("Failed to parse 2 goups, got %d", len(user.Groups))
|
|
||||||
} else {
|
|
||||||
if user.Groups[0] != "ping" {
|
|
||||||
t.Errorf("First group was %q, not expected value 'ping'", user.Groups[0])
|
|
||||||
}
|
|
||||||
if user.Groups[1] != "pong" {
|
|
||||||
t.Errorf("First group was %q, not expected value 'pong'", user.Groups[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !user.NoUserGroup {
|
|
||||||
t.Errorf("Failed to parse no-user-group field")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !user.System {
|
|
||||||
t.Errorf("Failed to parse system field")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !user.NoLogInit {
|
|
||||||
t.Errorf("Failed to parse no-log-init field")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TestUnitManager struct {
|
type TestUnitManager struct {
|
||||||
placed []string
|
placed []string
|
||||||
enabled []string
|
enabled []string
|
||||||
@@ -393,98 +39,263 @@ type UnitAction struct {
|
|||||||
command string
|
command string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tum *TestUnitManager) PlaceUnit(unit *system.Unit, dst string) error {
|
func (tum *TestUnitManager) PlaceUnit(u system.Unit) error {
|
||||||
tum.placed = append(tum.placed, unit.Name)
|
tum.placed = append(tum.placed, u.Name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (tum *TestUnitManager) EnableUnitFile(unit string, runtime bool) error {
|
func (tum *TestUnitManager) PlaceUnitDropIn(u system.Unit, d config.UnitDropIn) error {
|
||||||
tum.enabled = append(tum.enabled, unit)
|
tum.placed = append(tum.placed, u.Name+".d/"+d.Name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (tum *TestUnitManager) RunUnitCommand(command, unit string) (string, error) {
|
func (tum *TestUnitManager) EnableUnitFile(u system.Unit) error {
|
||||||
tum.commands = append(tum.commands, UnitAction{unit, command})
|
tum.enabled = append(tum.enabled, u.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (tum *TestUnitManager) RunUnitCommand(u system.Unit, c string) (string, error) {
|
||||||
|
tum.commands = append(tum.commands, UnitAction{u.Name, c})
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
func (tum *TestUnitManager) DaemonReload() error {
|
func (tum *TestUnitManager) DaemonReload() error {
|
||||||
tum.reload = true
|
tum.reload = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (tum *TestUnitManager) MaskUnit(unit *system.Unit) error {
|
func (tum *TestUnitManager) MaskUnit(u system.Unit) error {
|
||||||
tum.masked = append(tum.masked, unit.Name)
|
tum.masked = append(tum.masked, u.Name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (tum *TestUnitManager) UnmaskUnit(unit *system.Unit) error {
|
func (tum *TestUnitManager) UnmaskUnit(u system.Unit) error {
|
||||||
tum.unmasked = append(tum.unmasked, unit.Name)
|
tum.unmasked = append(tum.unmasked, u.Name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockInterface struct {
|
||||||
|
name string
|
||||||
|
filename string
|
||||||
|
netdev string
|
||||||
|
link string
|
||||||
|
network string
|
||||||
|
kind string
|
||||||
|
modprobeParams string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i mockInterface) Name() string {
|
||||||
|
return i.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i mockInterface) Filename() string {
|
||||||
|
return i.filename
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i mockInterface) Netdev() string {
|
||||||
|
return i.netdev
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i mockInterface) Link() string {
|
||||||
|
return i.link
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i mockInterface) Network() string {
|
||||||
|
return i.network
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i mockInterface) Type() string {
|
||||||
|
return i.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i mockInterface) ModprobeParams() string {
|
||||||
|
return i.modprobeParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateNetworkingUnits(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
interfaces []network.InterfaceGenerator
|
||||||
|
expect []system.Unit
|
||||||
|
}{
|
||||||
|
{nil, nil},
|
||||||
|
{
|
||||||
|
[]network.InterfaceGenerator{
|
||||||
|
network.InterfaceGenerator(mockInterface{filename: "test"}),
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]network.InterfaceGenerator{
|
||||||
|
network.InterfaceGenerator(mockInterface{filename: "test1", netdev: "test netdev"}),
|
||||||
|
network.InterfaceGenerator(mockInterface{filename: "test2", link: "test link"}),
|
||||||
|
network.InterfaceGenerator(mockInterface{filename: "test3", network: "test network"}),
|
||||||
|
},
|
||||||
|
[]system.Unit{
|
||||||
|
system.Unit{Unit: config.Unit{Name: "test1.netdev", Runtime: true, Content: "test netdev"}},
|
||||||
|
system.Unit{Unit: config.Unit{Name: "test2.link", Runtime: true, Content: "test link"}},
|
||||||
|
system.Unit{Unit: config.Unit{Name: "test3.network", Runtime: true, Content: "test network"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]network.InterfaceGenerator{
|
||||||
|
network.InterfaceGenerator(mockInterface{filename: "test", netdev: "test netdev", link: "test link", network: "test network"}),
|
||||||
|
},
|
||||||
|
[]system.Unit{
|
||||||
|
system.Unit{Unit: config.Unit{Name: "test.netdev", Runtime: true, Content: "test netdev"}},
|
||||||
|
system.Unit{Unit: config.Unit{Name: "test.link", Runtime: true, Content: "test link"}},
|
||||||
|
system.Unit{Unit: config.Unit{Name: "test.network", Runtime: true, Content: "test network"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
units := createNetworkingUnits(tt.interfaces)
|
||||||
|
if !reflect.DeepEqual(tt.expect, units) {
|
||||||
|
t.Errorf("bad units (%+v): want %#v, got %#v", tt.interfaces, tt.expect, units)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProcessUnits(t *testing.T) {
|
func TestProcessUnits(t *testing.T) {
|
||||||
tum := &TestUnitManager{}
|
tests := []struct {
|
||||||
units := []system.Unit{
|
units []system.Unit
|
||||||
system.Unit{
|
|
||||||
Name: "foo",
|
result TestUnitManager
|
||||||
Mask: true,
|
}{
|
||||||
|
{
|
||||||
|
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: "baz.service",
|
||||||
|
Content: "[Service]\nExecStart=/bin/baz",
|
||||||
|
Command: "start",
|
||||||
|
}},
|
||||||
|
system.Unit{Unit: config.Unit{
|
||||||
|
Name: "foo.network",
|
||||||
|
Content: "[Network]\nFoo=true",
|
||||||
|
}},
|
||||||
|
system.Unit{Unit: config.Unit{
|
||||||
|
Name: "bar.network",
|
||||||
|
Content: "[Network]\nBar=true",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
result: TestUnitManager{
|
||||||
|
placed: []string{"baz.service", "foo.network", "bar.network"},
|
||||||
|
commands: []UnitAction{
|
||||||
|
UnitAction{"systemd-networkd.service", "restart"},
|
||||||
|
UnitAction{"baz.service", "start"},
|
||||||
|
},
|
||||||
|
reload: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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{},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tum = &TestUnitManager{}
|
for _, tt := range tests {
|
||||||
units = []system.Unit{
|
tum := &TestUnitManager{}
|
||||||
system.Unit{
|
if err := processUnits(tt.units, "", tum); err != nil {
|
||||||
Name: "bar.network",
|
t.Errorf("bad error (%+v): want nil, got %s", tt.units, err)
|
||||||
},
|
}
|
||||||
}
|
if !reflect.DeepEqual(tt.result, *tum) {
|
||||||
if err := processUnits(units, "", tum); err != nil {
|
t.Errorf("bad result (%+v): want %+v, got %+v", tt.units, tt.result, tum)
|
||||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
}
|
||||||
}
|
|
||||||
if len(tum.commands) != 1 || (tum.commands[0] != UnitAction{"systemd-networkd.service", "restart"}) {
|
|
||||||
t.Errorf("expected systemd-networkd.service to be reloaded!")
|
|
||||||
}
|
|
||||||
|
|
||||||
tum = &TestUnitManager{}
|
|
||||||
units = []system.Unit{
|
|
||||||
system.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{
|
|
||||||
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{
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -22,6 +22,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -97,9 +98,9 @@ func (e *Environment) Apply(data string) string {
|
|||||||
|
|
||||||
func (e *Environment) DefaultEnvironmentFile() *system.EnvFile {
|
func (e *Environment) DefaultEnvironmentFile() *system.EnvFile {
|
||||||
ef := system.EnvFile{
|
ef := system.EnvFile{
|
||||||
File: &system.File{
|
File: &system.File{File: config.File{
|
||||||
Path: "/etc/environment",
|
Path: "/etc/environment",
|
||||||
},
|
}},
|
||||||
Vars: map[string]string{},
|
Vars: map[string]string{},
|
||||||
}
|
}
|
||||||
if ip, ok := e.substitutions["$public_ipv4"]; ok && len(ip) > 0 {
|
if ip, ok := e.substitutions["$public_ipv4"]; ok && len(ip) > 0 {
|
||||||
@@ -120,16 +121,3 @@ func (e *Environment) DefaultEnvironmentFile() *system.EnvFile {
|
|||||||
return &ef
|
return &ef
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeSvcEnv standardizes the keys of the map (environment variables for a service)
|
|
||||||
// by replacing any dashes with underscores and ensuring they are entirely upper case.
|
|
||||||
// For example, "some-env" --> "SOME_ENV"
|
|
||||||
func normalizeSvcEnv(m map[string]string) map[string]string {
|
|
||||||
out := make(map[string]string, len(m))
|
|
||||||
for key, val := range m {
|
|
||||||
key = strings.ToUpper(key)
|
|
||||||
key = strings.Replace(key, "-", "_", -1)
|
|
||||||
out[key] = val
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
@@ -1,63 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EtcdEnvironment map[string]string
|
|
||||||
|
|
||||||
func (ee EtcdEnvironment) String() (out string) {
|
|
||||||
norm := normalizeSvcEnv(ee)
|
|
||||||
|
|
||||||
if val, ok := norm["DISCOVERY_URL"]; ok {
|
|
||||||
delete(norm, "DISCOVERY_URL")
|
|
||||||
if _, ok := norm["DISCOVERY"]; !ok {
|
|
||||||
norm["DISCOVERY"] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sorted sort.StringSlice
|
|
||||||
for k, _ := range norm {
|
|
||||||
sorted = append(sorted, k)
|
|
||||||
}
|
|
||||||
sorted.Sort()
|
|
||||||
|
|
||||||
out += "[Service]\n"
|
|
||||||
|
|
||||||
for _, key := range sorted {
|
|
||||||
val := norm[key]
|
|
||||||
out += fmt.Sprintf("Environment=\"ETCD_%s=%s\"\n", key, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Units creates a Unit file drop-in for etcd, using any configured
|
|
||||||
// options and adding a default MachineID if unset.
|
|
||||||
func (ee EtcdEnvironment) Units(root string) ([]system.Unit, error) {
|
|
||||||
if len(ee) < 1 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := ee["name"]; !ok {
|
|
||||||
if machineID := system.MachineID(root); machineID != "" {
|
|
||||||
ee["name"] = machineID
|
|
||||||
} else if hostname, err := system.Hostname(); err == nil {
|
|
||||||
ee["name"] = hostname
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("Unable to determine default etcd name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
etcd := system.Unit{
|
|
||||||
Name: "etcd.service",
|
|
||||||
Runtime: true,
|
|
||||||
DropIn: true,
|
|
||||||
Content: ee.String(),
|
|
||||||
}
|
|
||||||
return []system.Unit{etcd}, nil
|
|
||||||
}
|
|
@@ -1,184 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEtcdEnvironment(t *testing.T) {
|
|
||||||
cfg := make(EtcdEnvironment, 0)
|
|
||||||
cfg["discovery"] = "http://disco.example.com/foobar"
|
|
||||||
cfg["peer-bind-addr"] = "127.0.0.1:7002"
|
|
||||||
|
|
||||||
env := cfg.String()
|
|
||||||
expect := `[Service]
|
|
||||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
|
||||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
|
||||||
`
|
|
||||||
|
|
||||||
if env != expect {
|
|
||||||
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEtcdEnvironmentDiscoveryURLTranslated(t *testing.T) {
|
|
||||||
cfg := make(EtcdEnvironment, 0)
|
|
||||||
cfg["discovery_url"] = "http://disco.example.com/foobar"
|
|
||||||
cfg["peer-bind-addr"] = "127.0.0.1:7002"
|
|
||||||
|
|
||||||
env := cfg.String()
|
|
||||||
expect := `[Service]
|
|
||||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
|
||||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
|
||||||
`
|
|
||||||
|
|
||||||
if env != expect {
|
|
||||||
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEtcdEnvironmentDiscoveryOverridesDiscoveryURL(t *testing.T) {
|
|
||||||
cfg := make(EtcdEnvironment, 0)
|
|
||||||
cfg["discovery_url"] = "ping"
|
|
||||||
cfg["discovery"] = "pong"
|
|
||||||
cfg["peer-bind-addr"] = "127.0.0.1:7002"
|
|
||||||
|
|
||||||
env := cfg.String()
|
|
||||||
expect := `[Service]
|
|
||||||
Environment="ETCD_DISCOVERY=pong"
|
|
||||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
|
||||||
`
|
|
||||||
|
|
||||||
if env != expect {
|
|
||||||
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEtcdEnvironmentWrittenToDisk(t *testing.T) {
|
|
||||||
ee := EtcdEnvironment{
|
|
||||||
"name": "node001",
|
|
||||||
"discovery": "http://disco.example.com/foobar",
|
|
||||||
"peer-bind-addr": "127.0.0.1:7002",
|
|
||||||
}
|
|
||||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to create tempdir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
|
|
||||||
sd := system.NewUnitManager(dir)
|
|
||||||
|
|
||||||
uu, err := ee.Units(dir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Generating etcd unit failed: %v", err)
|
|
||||||
}
|
|
||||||
if len(uu) != 1 {
|
|
||||||
t.Fatalf("Expected 1 unit to be returned, got %d", len(uu))
|
|
||||||
}
|
|
||||||
u := uu[0]
|
|
||||||
|
|
||||||
dst := u.Destination(dir)
|
|
||||||
os.Stderr.WriteString("writing to " + dir + "\n")
|
|
||||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
|
||||||
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
|
|
||||||
|
|
||||||
fi, err := os.Stat(fullPath)
|
|
||||||
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(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to read expected file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect := `[Service]
|
|
||||||
Environment="ETCD_DISCOVERY=http://disco.example.com/foobar"
|
|
||||||
Environment="ETCD_NAME=node001"
|
|
||||||
Environment="ETCD_PEER_BIND_ADDR=127.0.0.1:7002"
|
|
||||||
`
|
|
||||||
if string(contents) != expect {
|
|
||||||
t.Fatalf("File has incorrect contents")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEtcdEnvironmentEmptyNoOp(t *testing.T) {
|
|
||||||
ee := EtcdEnvironment{}
|
|
||||||
uu, err := ee.Units("")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if len(uu) > 0 {
|
|
||||||
t.Fatalf("Generated etcd units unexpectedly: %v", uu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEtcdEnvironmentWrittenToDiskDefaultToMachineID(t *testing.T) {
|
|
||||||
ee := EtcdEnvironment{"foo": "bar"}
|
|
||||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to create tempdir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
|
|
||||||
sd := system.NewUnitManager(dir)
|
|
||||||
|
|
||||||
os.Mkdir(path.Join(dir, "etc"), os.FileMode(0755))
|
|
||||||
err = ioutil.WriteFile(path.Join(dir, "etc", "machine-id"), []byte("node007"), os.FileMode(0444))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed writing out /etc/machine-id: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
uu, err := ee.Units(dir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Generating etcd unit failed: %v", err)
|
|
||||||
}
|
|
||||||
if len(uu) == 0 {
|
|
||||||
t.Fatalf("Returned empty etcd units unexpectedly")
|
|
||||||
}
|
|
||||||
u := uu[0]
|
|
||||||
|
|
||||||
dst := u.Destination(dir)
|
|
||||||
os.Stderr.WriteString("writing to " + dir + "\n")
|
|
||||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
|
||||||
t.Fatalf("Writing of EtcdEnvironment failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fullPath := path.Join(dir, "run", "systemd", "system", "etcd.service.d", "20-cloudinit.conf")
|
|
||||||
|
|
||||||
contents, err := ioutil.ReadFile(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to read expected file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect := `[Service]
|
|
||||||
Environment="ETCD_FOO=bar"
|
|
||||||
Environment="ETCD_NAME=node007"
|
|
||||||
`
|
|
||||||
if string(contents) != expect {
|
|
||||||
t.Fatalf("File has incorrect contents")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEtcdEnvironmentWhenNil(t *testing.T) {
|
|
||||||
// EtcdEnvironment will be a nil map if it wasn't in the yaml
|
|
||||||
var ee EtcdEnvironment
|
|
||||||
if ee != nil {
|
|
||||||
t.Fatalf("EtcdEnvironment is not nil")
|
|
||||||
}
|
|
||||||
uu, err := ee.Units("")
|
|
||||||
if len(uu) != 0 || err != nil {
|
|
||||||
t.Fatalf("Units returned value for nil input")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FleetEnvironment map[string]string
|
|
||||||
|
|
||||||
func (fe FleetEnvironment) String() (out string) {
|
|
||||||
norm := normalizeSvcEnv(fe)
|
|
||||||
out += "[Service]\n"
|
|
||||||
|
|
||||||
for key, val := range norm {
|
|
||||||
out += fmt.Sprintf("Environment=\"FLEET_%s=%s\"\n", key, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Units generates a Unit file drop-in for fleet, if any fleet options were
|
|
||||||
// configured in cloud-config
|
|
||||||
func (fe FleetEnvironment) Units(root string) ([]system.Unit, error) {
|
|
||||||
if len(fe) < 1 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
fleet := system.Unit{
|
|
||||||
Name: "fleet.service",
|
|
||||||
Runtime: true,
|
|
||||||
DropIn: true,
|
|
||||||
Content: fe.String(),
|
|
||||||
}
|
|
||||||
return []system.Unit{fleet}, nil
|
|
||||||
}
|
|
@@ -1,43 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestFleetEnvironment(t *testing.T) {
|
|
||||||
cfg := make(FleetEnvironment, 0)
|
|
||||||
cfg["public-ip"] = "12.34.56.78"
|
|
||||||
|
|
||||||
env := cfg.String()
|
|
||||||
|
|
||||||
expect := `[Service]
|
|
||||||
Environment="FLEET_PUBLIC_IP=12.34.56.78"
|
|
||||||
`
|
|
||||||
|
|
||||||
if env != expect {
|
|
||||||
t.Errorf("Generated environment:\n%s\nExpected environment:\n%s", env, expect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFleetUnit(t *testing.T) {
|
|
||||||
cfg := make(FleetEnvironment, 0)
|
|
||||||
uu, err := cfg.Units("/")
|
|
||||||
if len(uu) != 0 {
|
|
||||||
t.Errorf("unexpectedly generated unit with empty FleetEnvironment")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg["public-ip"] = "12.34.56.78"
|
|
||||||
|
|
||||||
uu, err = cfg.Units("/")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("error generating fleet unit: %v", err)
|
|
||||||
}
|
|
||||||
if len(uu) != 1 {
|
|
||||||
t.Fatalf("expected 1 unit generated, got %d", len(uu))
|
|
||||||
}
|
|
||||||
u := uu[0]
|
|
||||||
if !u.Runtime {
|
|
||||||
t.Errorf("bad Runtime for generated fleet unit!")
|
|
||||||
}
|
|
||||||
if !u.DropIn {
|
|
||||||
t.Errorf("bad DropIn for generated fleet unit!")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,32 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCloudConfigUsersGithubUser(t *testing.T) {
|
|
||||||
|
|
||||||
contents := `
|
|
||||||
users:
|
|
||||||
- name: elroy
|
|
||||||
coreos-ssh-import-github: bcwaldon
|
|
||||||
`
|
|
||||||
cfg, err := NewCloudConfig(contents)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Encountered unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.Users) != 1 {
|
|
||||||
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
|
|
||||||
}
|
|
||||||
|
|
||||||
user := cfg.Users[0]
|
|
||||||
|
|
||||||
if user.Name != "elroy" {
|
|
||||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.SSHImportGithubUser != "bcwaldon" {
|
|
||||||
t.Errorf("github user is %q, expected 'bcwaldon'", user.SSHImportGithubUser)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,83 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCloudConfigManageEtcHosts(t *testing.T) {
|
|
||||||
contents := `
|
|
||||||
manage_etc_hosts: localhost
|
|
||||||
`
|
|
||||||
cfg, err := NewCloudConfig(contents)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Encountered unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
manageEtcHosts := cfg.ManageEtcHosts
|
|
||||||
|
|
||||||
if manageEtcHosts != "localhost" {
|
|
||||||
t.Errorf("ManageEtcHosts value is %q, expected 'localhost'", manageEtcHosts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManageEtcHostsInvalidValue(t *testing.T) {
|
|
||||||
eh := EtcHosts("invalid")
|
|
||||||
if f, err := eh.File(""); err == nil || f != nil {
|
|
||||||
t.Fatalf("EtcHosts File succeeded with invalid value!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEtcHostsWrittenToDisk(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)
|
|
||||||
|
|
||||||
eh := EtcHosts("localhost")
|
|
||||||
|
|
||||||
f, err := eh.File(dir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error calling File on EtcHosts: %v", err)
|
|
||||||
}
|
|
||||||
if f == nil {
|
|
||||||
t.Fatalf("manageEtcHosts returned nil file unexpectedly")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := system.WriteFile(f, dir); err != nil {
|
|
||||||
t.Fatalf("Error writing EtcHosts: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fullPath := path.Join(dir, "etc", "hosts")
|
|
||||||
|
|
||||||
fi, err := os.Stat(fullPath)
|
|
||||||
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(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to read expected file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to read OS hostname: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect := fmt.Sprintf("%s %s\n", DefaultIpv4Address, hostname)
|
|
||||||
|
|
||||||
if string(contents) != expect {
|
|
||||||
t.Fatalf("File has incorrect contents")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -19,11 +19,13 @@ package initialize
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseMetaData parses a JSON blob in the OpenStack metadata service format,
|
// ParseMetaData parses a JSON blob in the OpenStack metadata service format,
|
||||||
// and converts it to a partially hydrated CloudConfig.
|
// and converts it to a partially hydrated CloudConfig.
|
||||||
func ParseMetaData(contents string) (*CloudConfig, error) {
|
func ParseMetaData(contents string) (*config.CloudConfig, error) {
|
||||||
if len(contents) == 0 {
|
if len(contents) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -38,7 +40,7 @@ func ParseMetaData(contents string) (*CloudConfig, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg CloudConfig
|
var cfg config.CloudConfig
|
||||||
if len(metadata.SSHAuthorizedKeyMap) > 0 {
|
if len(metadata.SSHAuthorizedKeyMap) > 0 {
|
||||||
cfg.SSHAuthorizedKeys = make([]string, 0, len(metadata.SSHAuthorizedKeyMap))
|
cfg.SSHAuthorizedKeys = make([]string, 0, len(metadata.SSHAuthorizedKeyMap))
|
||||||
for _, name := range sortedKeys(metadata.SSHAuthorizedKeyMap) {
|
for _, name := range sortedKeys(metadata.SSHAuthorizedKeyMap) {
|
||||||
|
@@ -16,22 +16,26 @@
|
|||||||
|
|
||||||
package initialize
|
package initialize
|
||||||
|
|
||||||
import "reflect"
|
import (
|
||||||
import "testing"
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
func TestParseMetadata(t *testing.T) {
|
func TestParseMetadata(t *testing.T) {
|
||||||
for i, tt := range []struct {
|
for i, tt := range []struct {
|
||||||
in string
|
in string
|
||||||
want *CloudConfig
|
want *config.CloudConfig
|
||||||
err bool
|
err bool
|
||||||
}{
|
}{
|
||||||
{"", nil, false},
|
{"", nil, false},
|
||||||
{`garbage, invalid json`, nil, true},
|
{`garbage, invalid json`, nil, true},
|
||||||
{`{"foo": "bar"}`, &CloudConfig{}, false},
|
{`{"foo": "bar"}`, &config.CloudConfig{}, false},
|
||||||
{`{"network_config": {"content_path": "asdf"}}`, &CloudConfig{NetworkConfigPath: "asdf"}, false},
|
{`{"network_config": {"content_path": "asdf"}}`, &config.CloudConfig{NetworkConfigPath: "asdf"}, false},
|
||||||
{`{"hostname": "turkleton"}`, &CloudConfig{Hostname: "turkleton"}, false},
|
{`{"hostname": "turkleton"}`, &config.CloudConfig{Hostname: "turkleton"}, false},
|
||||||
{`{"public_keys": {"jack": "jill", "bob": "alice"}}`, &CloudConfig{SSHAuthorizedKeys: []string{"alice", "jill"}}, false},
|
{`{"public_keys": {"jack": "jill", "bob": "alice"}}`, &config.CloudConfig{SSHAuthorizedKeys: []string{"alice", "jill"}}, false},
|
||||||
{`{"unknown": "thing", "hostname": "my_host", "public_keys": {"do": "re", "mi": "fa"}, "network_config": {"content_path": "/root", "blah": "zzz"}}`, &CloudConfig{SSHAuthorizedKeys: []string{"re", "fa"}, Hostname: "my_host", NetworkConfigPath: "/root"}, false},
|
{`{"unknown": "thing", "hostname": "my_host", "public_keys": {"do": "re", "mi": "fa"}, "network_config": {"content_path": "/root", "blah": "zzz"}}`, &config.CloudConfig{SSHAuthorizedKeys: []string{"re", "fa"}, Hostname: "my_host", NetworkConfigPath: "/root"}, false},
|
||||||
} {
|
} {
|
||||||
got, err := ParseMetaData(tt.in)
|
got, err := ParseMetaData(tt.in)
|
||||||
if tt.err != (err != nil) {
|
if tt.err != (err != nil) {
|
||||||
|
@@ -1,41 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OEMRelease 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (oem OEMRelease) String() string {
|
|
||||||
fields := []string{
|
|
||||||
fmt.Sprintf("ID=%s", oem.ID),
|
|
||||||
fmt.Sprintf("VERSION_ID=%s", oem.VersionID),
|
|
||||||
fmt.Sprintf("NAME=%q", oem.Name),
|
|
||||||
fmt.Sprintf("HOME_URL=%q", oem.HomeURL),
|
|
||||||
fmt.Sprintf("BUG_REPORT_URL=%q", oem.BugReportURL),
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(fields, "\n") + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (oem OEMRelease) File(root string) (*system.File, error) {
|
|
||||||
if oem.ID == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &system.File{
|
|
||||||
Path: path.Join("etc", "oem-release"),
|
|
||||||
RawFilePermissions: "0644",
|
|
||||||
Content: oem.String(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
@@ -1,63 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOEMReleaseWrittenToDisk(t *testing.T) {
|
|
||||||
oem := OEMRelease{
|
|
||||||
ID: "rackspace",
|
|
||||||
Name: "Rackspace Cloud Servers",
|
|
||||||
VersionID: "168.0.0",
|
|
||||||
HomeURL: "https://www.rackspace.com/cloud/servers/",
|
|
||||||
BugReportURL: "https://github.com/coreos/coreos-overlay",
|
|
||||||
}
|
|
||||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to create tempdir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
|
|
||||||
f, err := oem.File(dir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Processing of OEMRelease failed: %v", err)
|
|
||||||
}
|
|
||||||
if f == nil {
|
|
||||||
t.Fatalf("OEMRelease returned nil file unexpectedly")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := system.WriteFile(f, dir); err != nil {
|
|
||||||
t.Fatalf("Writing of OEMRelease failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fullPath := path.Join(dir, "etc", "oem-release")
|
|
||||||
|
|
||||||
fi, err := os.Stat(fullPath)
|
|
||||||
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(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to read expected file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect := `ID=rackspace
|
|
||||||
VERSION_ID=168.0.0
|
|
||||||
NAME="Rackspace Cloud Servers"
|
|
||||||
HOME_URL="https://www.rackspace.com/cloud/servers/"
|
|
||||||
BUG_REPORT_URL="https://github.com/coreos/coreos-overlay"
|
|
||||||
`
|
|
||||||
if string(contents) != expect {
|
|
||||||
t.Fatalf("File has incorrect contents")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -55,31 +55,4 @@ func TestCloudConfigUsersUrlMarshal(t *testing.T) {
|
|||||||
if keys[2] != expected {
|
if keys[2] != expected {
|
||||||
t.Fatalf("expected %s, got %s", expected, keys[2])
|
t.Fatalf("expected %s, got %s", expected, keys[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
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
|
|
||||||
`
|
|
||||||
cfg, err := NewCloudConfig(contents)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Encountered unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.Users) != 1 {
|
|
||||||
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
|
|
||||||
}
|
|
||||||
|
|
||||||
user := cfg.Users[0]
|
|
||||||
|
|
||||||
if user.Name != "elroy" {
|
|
||||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.SSHImportURL != "https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys" {
|
|
||||||
t.Errorf("ssh import url is %q, expected 'https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys'", user.SSHImportURL)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,165 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
locksmithUnit = "locksmithd.service"
|
|
||||||
updateEngineUnit = "update-engine.service"
|
|
||||||
)
|
|
||||||
|
|
||||||
// updateOption represents a configurable update option, which, if set, will be
|
|
||||||
// written into update.conf, replacing any existing value for the option
|
|
||||||
type updateOption struct {
|
|
||||||
key string // key used to configure this option in cloud-config
|
|
||||||
valid []string // valid values for the option
|
|
||||||
prefix string // prefix for the option in the update.conf file
|
|
||||||
value string // used to store the new value in update.conf (including prefix)
|
|
||||||
seen bool // whether the option has been seen in any existing update.conf
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateOptions defines the update options understood by cloud-config.
|
|
||||||
// The keys represent the string used in cloud-config to configure the option.
|
|
||||||
var updateOptions = []*updateOption{
|
|
||||||
&updateOption{
|
|
||||||
key: "reboot-strategy",
|
|
||||||
prefix: "REBOOT_STRATEGY=",
|
|
||||||
valid: []string{"best-effort", "etcd-lock", "reboot", "off"},
|
|
||||||
},
|
|
||||||
&updateOption{
|
|
||||||
key: "group",
|
|
||||||
prefix: "GROUP=",
|
|
||||||
},
|
|
||||||
&updateOption{
|
|
||||||
key: "server",
|
|
||||||
prefix: "SERVER=",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValid checks whether a supplied value is valid for this option
|
|
||||||
func (uo updateOption) isValid(val string) bool {
|
|
||||||
if len(uo.valid) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, v := range uo.valid {
|
|
||||||
if val == v {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateConfig map[string]string
|
|
||||||
|
|
||||||
// File generates an `/etc/coreos/update.conf` file (if any update
|
|
||||||
// configuration options are set in cloud-config) by either rewriting the
|
|
||||||
// existing file on disk, or starting from `/usr/share/coreos/update.conf`
|
|
||||||
func (uc UpdateConfig) File(root string) (*system.File, error) {
|
|
||||||
if len(uc) < 1 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var out string
|
|
||||||
|
|
||||||
// Generate the list of possible substitutions to be performed based on the options that are configured
|
|
||||||
subs := make([]*updateOption, 0)
|
|
||||||
for _, uo := range updateOptions {
|
|
||||||
val, ok := uc[uo.key]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !uo.isValid(val) {
|
|
||||||
return nil, errors.New(fmt.Sprintf("invalid value %v for option %v (valid options: %v)", val, uo.key, uo.valid))
|
|
||||||
}
|
|
||||||
uo.value = uo.prefix + val
|
|
||||||
subs = append(subs, uo)
|
|
||||||
}
|
|
||||||
|
|
||||||
etcUpdate := path.Join(root, "etc", "coreos", "update.conf")
|
|
||||||
usrUpdate := path.Join(root, "usr", "share", "coreos", "update.conf")
|
|
||||||
|
|
||||||
conf, err := os.Open(etcUpdate)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
conf, err = os.Open(usrUpdate)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(conf)
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
for _, s := range subs {
|
|
||||||
if strings.HasPrefix(line, s.prefix) {
|
|
||||||
line = s.value
|
|
||||||
s.seen = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out += line
|
|
||||||
out += "\n"
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range subs {
|
|
||||||
if !s.seen {
|
|
||||||
out += s.value
|
|
||||||
out += "\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &system.File{
|
|
||||||
Path: path.Join("etc", "coreos", "update.conf"),
|
|
||||||
RawFilePermissions: "0644",
|
|
||||||
Content: out,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Units generates units for the cloud-init initializer to act on:
|
|
||||||
// - a locksmith system.Unit, if "reboot-strategy" was set in cloud-config
|
|
||||||
// - an update_engine system.Unit, if "group" was set in cloud-config
|
|
||||||
func (uc UpdateConfig) Units(root string) ([]system.Unit, error) {
|
|
||||||
var units []system.Unit
|
|
||||||
if strategy, ok := uc["reboot-strategy"]; ok {
|
|
||||||
ls := &system.Unit{
|
|
||||||
Name: locksmithUnit,
|
|
||||||
Command: "restart",
|
|
||||||
Mask: false,
|
|
||||||
Runtime: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if strategy == "off" {
|
|
||||||
ls.Command = "stop"
|
|
||||||
ls.Mask = true
|
|
||||||
}
|
|
||||||
units = append(units, *ls)
|
|
||||||
}
|
|
||||||
|
|
||||||
rue := false
|
|
||||||
if _, ok := uc["group"]; ok {
|
|
||||||
rue = true
|
|
||||||
}
|
|
||||||
if _, ok := uc["server"]; ok {
|
|
||||||
rue = true
|
|
||||||
}
|
|
||||||
if rue {
|
|
||||||
ue := system.Unit{
|
|
||||||
Name: updateEngineUnit,
|
|
||||||
Command: "restart",
|
|
||||||
}
|
|
||||||
units = append(units, ue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return units, nil
|
|
||||||
}
|
|
@@ -1,232 +0,0 @@
|
|||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
base = `SERVER=https://example.com
|
|
||||||
GROUP=thegroupc`
|
|
||||||
configured = base + `
|
|
||||||
REBOOT_STRATEGY=awesome
|
|
||||||
`
|
|
||||||
expected = base + `
|
|
||||||
REBOOT_STRATEGY=etcd-lock
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupFixtures(dir string) {
|
|
||||||
os.MkdirAll(path.Join(dir, "usr", "share", "coreos"), 0755)
|
|
||||||
os.MkdirAll(path.Join(dir, "run", "systemd", "system"), 0755)
|
|
||||||
|
|
||||||
ioutil.WriteFile(path.Join(dir, "usr", "share", "coreos", "update.conf"), []byte(base), 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmptyUpdateConfig(t *testing.T) {
|
|
||||||
uc := &UpdateConfig{}
|
|
||||||
f, err := uc.File("")
|
|
||||||
if err != nil {
|
|
||||||
t.Error("unexpected error getting file from empty UpdateConfig")
|
|
||||||
}
|
|
||||||
if f != nil {
|
|
||||||
t.Errorf("getting file from empty UpdateConfig should have returned nil, got %v", f)
|
|
||||||
}
|
|
||||||
uu, err := uc.Units("")
|
|
||||||
if err != nil {
|
|
||||||
t.Error("unexpected error getting unit from empty UpdateConfig")
|
|
||||||
}
|
|
||||||
if len(uu) != 0 {
|
|
||||||
t.Errorf("getting unit from empty UpdateConfig should have returned zero units, got %d", len(uu))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidUpdateOptions(t *testing.T) {
|
|
||||||
uon := &updateOption{
|
|
||||||
key: "numbers",
|
|
||||||
prefix: "numero_",
|
|
||||||
valid: []string{"one", "two"},
|
|
||||||
}
|
|
||||||
uoa := &updateOption{
|
|
||||||
key: "any_will_do",
|
|
||||||
prefix: "any_",
|
|
||||||
}
|
|
||||||
|
|
||||||
if !uon.isValid("one") {
|
|
||||||
t.Error("update option did not accept valid option \"one\"")
|
|
||||||
}
|
|
||||||
if uon.isValid("three") {
|
|
||||||
t.Error("update option accepted invalid option \"three\"")
|
|
||||||
}
|
|
||||||
for _, s := range []string{"one", "asdf", "foobarbaz"} {
|
|
||||||
if !uoa.isValid(s) {
|
|
||||||
t.Errorf("update option with no \"valid\" field did not accept %q", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uc := &UpdateConfig{"reboot-strategy": "wizzlewazzle"}
|
|
||||||
f, err := uc.File("")
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("File did not give an error on invalid UpdateOption")
|
|
||||||
}
|
|
||||||
if f != nil {
|
|
||||||
t.Errorf("File did not return a nil file on invalid UpdateOption")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerGroupOptions(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)
|
|
||||||
setupFixtures(dir)
|
|
||||||
u := &UpdateConfig{"group": "master", "server": "http://foo.com"}
|
|
||||||
|
|
||||||
want := `
|
|
||||||
GROUP=master
|
|
||||||
SERVER=http://foo.com`
|
|
||||||
|
|
||||||
f, err := u.File(dir)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error getting file from UpdateConfig: %v", err)
|
|
||||||
} else if f == nil {
|
|
||||||
t.Error("unexpectedly got empty file from UpdateConfig")
|
|
||||||
} else {
|
|
||||||
out := strings.Split(f.Content, "\n")
|
|
||||||
sort.Strings(out)
|
|
||||||
got := strings.Join(out, "\n")
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("File has incorrect contents, got %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uu, err := u.Units(dir)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error getting units from UpdateConfig: %v", err)
|
|
||||||
} else if len(uu) != 1 {
|
|
||||||
t.Errorf("unexpected number of files returned from UpdateConfig: want 1, got %d", len(uu))
|
|
||||||
} else {
|
|
||||||
unit := uu[0]
|
|
||||||
if unit.Name != "update-engine.service" {
|
|
||||||
t.Errorf("bad name for generated unit: want update-engine.service, got %s", unit.Name)
|
|
||||||
}
|
|
||||||
if unit.Command != "restart" {
|
|
||||||
t.Errorf("bad command for generated unit: want restart, got %s", unit.Command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRebootStrategies(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)
|
|
||||||
setupFixtures(dir)
|
|
||||||
strategies := []struct {
|
|
||||||
name string
|
|
||||||
line string
|
|
||||||
uMask bool
|
|
||||||
uCommand string
|
|
||||||
}{
|
|
||||||
{"best-effort", "REBOOT_STRATEGY=best-effort", false, "restart"},
|
|
||||||
{"etcd-lock", "REBOOT_STRATEGY=etcd-lock", false, "restart"},
|
|
||||||
{"reboot", "REBOOT_STRATEGY=reboot", false, "restart"},
|
|
||||||
{"off", "REBOOT_STRATEGY=off", true, "stop"},
|
|
||||||
}
|
|
||||||
for _, s := range strategies {
|
|
||||||
uc := &UpdateConfig{"reboot-strategy": s.name}
|
|
||||||
f, err := uc.File(dir)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("update failed to generate file for reboot-strategy=%v: %v", s.name, err)
|
|
||||||
} else if f == nil {
|
|
||||||
t.Errorf("generated empty file for reboot-strategy=%v", s.name)
|
|
||||||
} else {
|
|
||||||
seen := false
|
|
||||||
for _, line := range strings.Split(f.Content, "\n") {
|
|
||||||
if line == s.line {
|
|
||||||
seen = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !seen {
|
|
||||||
t.Errorf("couldn't find expected line %v for reboot-strategy=%v", s.line, s.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uu, err := uc.Units(dir)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to generate unit for reboot-strategy=%v!", s.name)
|
|
||||||
} else if len(uu) != 1 {
|
|
||||||
t.Errorf("unexpected number of units for reboot-strategy=%v: %d", s.name, len(uu))
|
|
||||||
} else {
|
|
||||||
u := uu[0]
|
|
||||||
if u.Name != locksmithUnit {
|
|
||||||
t.Errorf("unit generated for reboot strategy=%v had bad name: %v", s.name, u.Name)
|
|
||||||
}
|
|
||||||
if u.Mask != s.uMask {
|
|
||||||
t.Errorf("unit generated for reboot strategy=%v had bad mask: %t", s.name, u.Mask)
|
|
||||||
}
|
|
||||||
if u.Command != s.uCommand {
|
|
||||||
t.Errorf("unit generated for reboot strategy=%v had bad command: %v", s.name, u.Command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateConfWrittenToDisk(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)
|
|
||||||
setupFixtures(dir)
|
|
||||||
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
if i == 1 {
|
|
||||||
err = ioutil.WriteFile(path.Join(dir, "etc", "coreos", "update.conf"), []byte(configured), 0644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uc := &UpdateConfig{"reboot-strategy": "etcd-lock"}
|
|
||||||
|
|
||||||
f, err := uc.File(dir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Processing UpdateConfig failed: %v", err)
|
|
||||||
} else if f == nil {
|
|
||||||
t.Fatal("Unexpectedly got nil updateconfig file")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := system.WriteFile(f, dir); err != nil {
|
|
||||||
t.Fatalf("Error writing update config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fullPath := path.Join(dir, "etc", "coreos", "update.conf")
|
|
||||||
|
|
||||||
fi, err := os.Stat(fullPath)
|
|
||||||
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(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to read expected file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(contents) != expected {
|
|
||||||
t.Fatalf("File has incorrect contents, got %v, wanted %v", string(contents), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -17,31 +17,25 @@
|
|||||||
package initialize
|
package initialize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseUserData(contents string) (interface{}, error) {
|
func ParseUserData(contents string) (interface{}, error) {
|
||||||
if len(contents) == 0 {
|
if len(contents) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
header := strings.SplitN(contents, "\n", 2)[0]
|
|
||||||
|
|
||||||
// Explicitly trim the header so we can handle user-data from
|
switch {
|
||||||
// non-unix operating systems. The rest of the file is parsed
|
case config.IsScript(contents):
|
||||||
// by yaml, which correctly handles CRLF.
|
|
||||||
header = strings.TrimSpace(header)
|
|
||||||
|
|
||||||
if strings.HasPrefix(header, "#!") {
|
|
||||||
log.Printf("Parsing user-data as script")
|
log.Printf("Parsing user-data as script")
|
||||||
return system.Script(contents), nil
|
return config.NewScript(contents)
|
||||||
} else if header == "#cloud-config" {
|
case config.IsCloudConfig(contents):
|
||||||
log.Printf("Parsing user-data as cloud-config")
|
log.Printf("Parsing user-data as cloud-config")
|
||||||
return NewCloudConfig(contents)
|
return config.NewCloudConfig(contents)
|
||||||
} else {
|
default:
|
||||||
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
|
return nil, errors.New("Unrecognized user-data format")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,8 @@ package initialize
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseHeaderCRLF(t *testing.T) {
|
func TestParseHeaderCRLF(t *testing.T) {
|
||||||
@@ -53,7 +55,7 @@ func TestParseConfigCRLF(t *testing.T) {
|
|||||||
t.Fatalf("Failed parsing config: %v", err)
|
t.Fatalf("Failed parsing config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := ud.(*CloudConfig)
|
cfg := ud.(*config.CloudConfig)
|
||||||
|
|
||||||
if cfg.Hostname != "foo" {
|
if cfg.Hostname != "foo" {
|
||||||
t.Error("Failed parsing hostname from config")
|
t.Error("Failed parsing hostname from config")
|
||||||
|
@@ -21,6 +21,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ func PrepWorkspace(workspace string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PersistScriptInWorkspace(script system.Script, workspace string) (string, error) {
|
func PersistScriptInWorkspace(script config.Script, workspace string) (string, error) {
|
||||||
scriptsPath := path.Join(workspace, "scripts")
|
scriptsPath := path.Join(workspace, "scripts")
|
||||||
tmp, err := ioutil.TempFile(scriptsPath, "")
|
tmp, err := ioutil.TempFile(scriptsPath, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -47,21 +48,21 @@ func PersistScriptInWorkspace(script system.Script, workspace string) (string, e
|
|||||||
|
|
||||||
relpath := strings.TrimPrefix(tmp.Name(), workspace)
|
relpath := strings.TrimPrefix(tmp.Name(), workspace)
|
||||||
|
|
||||||
file := system.File{
|
file := system.File{File: config.File{
|
||||||
Path: relpath,
|
Path: relpath,
|
||||||
RawFilePermissions: "0744",
|
RawFilePermissions: "0744",
|
||||||
Content: string(script),
|
Content: string(script),
|
||||||
}
|
}}
|
||||||
|
|
||||||
return system.WriteFile(&file, workspace)
|
return system.WriteFile(&file, workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PersistUnitNameInWorkspace(name string, workspace string) error {
|
func PersistUnitNameInWorkspace(name string, workspace string) error {
|
||||||
file := system.File{
|
file := system.File{File: config.File{
|
||||||
Path: path.Join("scripts", "unit-name"),
|
Path: path.Join("scripts", "unit-name"),
|
||||||
RawFilePermissions: "0644",
|
RawFilePermissions: "0644",
|
||||||
Content: name,
|
Content: name,
|
||||||
}
|
}}
|
||||||
_, err := system.WriteFile(&file, workspace)
|
_, err := system.WriteFile(&file, workspace)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -19,19 +19,21 @@ package system
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dropinContents generates the contents for a drop-in unit given the config.
|
// dropinContents generates the contents for a drop-in unit given the config.
|
||||||
// The argument must be a struct from the 'config' package.
|
// The argument must be a struct from the 'config' package.
|
||||||
func dropinContents(e interface{}) string {
|
func serviceContents(e interface{}) string {
|
||||||
et := reflect.TypeOf(e)
|
et := reflect.TypeOf(e)
|
||||||
ev := reflect.ValueOf(e)
|
ev := reflect.ValueOf(e)
|
||||||
|
|
||||||
var out string
|
var out string
|
||||||
for i := 0; i < et.NumField(); i++ {
|
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")
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,6 +23,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -64,9 +66,9 @@ func TestWriteEnvFileUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueUpdate,
|
Vars: valueUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,9 +113,9 @@ func TestWriteEnvFileUpdateNoNewline(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueUpdate,
|
Vars: valueUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,9 +154,9 @@ func TestWriteEnvFileCreate(t *testing.T) {
|
|||||||
fullPath := path.Join(dir, name)
|
fullPath := path.Join(dir, name)
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueUpdate,
|
Vars: valueUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,9 +192,9 @@ func TestWriteEnvFileNoop(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueNoop,
|
Vars: valueNoop,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,9 +239,9 @@ func TestWriteEnvFileUpdateDos(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueUpdate,
|
Vars: valueUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,9 +288,9 @@ func TestWriteEnvFileDos2Unix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueNoop,
|
Vars: valueNoop,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,9 +336,9 @@ func TestWriteEnvFileEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueEmpty,
|
Vars: valueEmpty,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,9 +378,9 @@ func TestWriteEnvFileEmptyNoCreate(t *testing.T) {
|
|||||||
fullPath := path.Join(dir, name)
|
fullPath := path.Join(dir, name)
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueEmpty,
|
Vars: valueEmpty,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,9 +409,9 @@ func TestWriteEnvFilePermFailure(t *testing.T) {
|
|||||||
ioutil.WriteFile(fullPath, []byte(base), 0000)
|
ioutil.WriteFile(fullPath, []byte(base), 0000)
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueUpdate,
|
Vars: valueUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,9 +431,9 @@ func TestWriteEnvFileNameFailure(t *testing.T) {
|
|||||||
name := "foo.conf"
|
name := "foo.conf"
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueInvalid,
|
Vars: valueInvalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -14,7 +14,7 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package initialize
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -22,15 +22,17 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultIpv4Address = "127.0.0.1"
|
const DefaultIpv4Address = "127.0.0.1"
|
||||||
|
|
||||||
type EtcHosts string
|
type EtcHosts struct {
|
||||||
|
config.EtcHosts
|
||||||
|
}
|
||||||
|
|
||||||
func (eh EtcHosts) generateEtcHosts() (out string, err error) {
|
func (eh EtcHosts) generateEtcHosts() (out string, err error) {
|
||||||
if eh != "localhost" {
|
if eh.EtcHosts != "localhost" {
|
||||||
return "", errors.New("Invalid option to manage_etc_hosts")
|
return "", errors.New("Invalid option to manage_etc_hosts")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +46,8 @@ func (eh EtcHosts) generateEtcHosts() (out string, err error) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (eh EtcHosts) File(root string) (*system.File, error) {
|
func (eh EtcHosts) File() (*File, error) {
|
||||||
if eh == "" {
|
if eh.EtcHosts == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +56,9 @@ func (eh EtcHosts) File(root string) (*system.File, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &system.File{
|
return &File{config.File{
|
||||||
Path: path.Join("etc", "hosts"),
|
Path: path.Join("etc", "hosts"),
|
||||||
RawFilePermissions: "0644",
|
RawFilePermissions: "0644",
|
||||||
Content: etcHosts,
|
Content: etcHosts,
|
||||||
}, nil
|
}}, nil
|
||||||
}
|
}
|
62
system/etc_hosts_test.go
Normal file
62
system/etc_hosts_test.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEtcdHostsFile(t *testing.T) {
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range []struct {
|
||||||
|
config config.EtcHosts
|
||||||
|
file *File
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"invalid",
|
||||||
|
nil,
|
||||||
|
fmt.Errorf("Invalid option to manage_etc_hosts"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"localhost",
|
||||||
|
&File{config.File{
|
||||||
|
Content: fmt.Sprintf("127.0.0.1 %s\n", hostname),
|
||||||
|
Path: "etc/hosts",
|
||||||
|
RawFilePermissions: "0644",
|
||||||
|
}},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
file, err := EtcHosts{tt.config}.File()
|
||||||
|
if !reflect.DeepEqual(tt.err, err) {
|
||||||
|
t.Errorf("bad error (%q): want %q, got %q", tt.config, tt.err, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(tt.file, file) {
|
||||||
|
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.file, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
system/etcd.go
Normal file
39
system/etcd.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Etcd is a top-level structure which embeds its underlying configuration,
|
||||||
|
// config.Etcd, and provides the system-specific Unit().
|
||||||
|
type Etcd struct {
|
||||||
|
config.Etcd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Units creates a Unit file drop-in for etcd, using any configured options.
|
||||||
|
func (ee Etcd) Units() []Unit {
|
||||||
|
return []Unit{{config.Unit{
|
||||||
|
Name: "etcd.service",
|
||||||
|
Runtime: true,
|
||||||
|
DropIns: []config.UnitDropIn{{
|
||||||
|
Name: "20-cloudinit.conf",
|
||||||
|
Content: serviceContents(ee.Etcd),
|
||||||
|
}},
|
||||||
|
}}}
|
||||||
|
}
|
81
system/etcd_test.go
Normal file
81
system/etcd_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEtcdUnits(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
config config.Etcd
|
||||||
|
units []Unit
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
config.Etcd{},
|
||||||
|
[]Unit{{config.Unit{
|
||||||
|
Name: "etcd.service",
|
||||||
|
Runtime: true,
|
||||||
|
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config.Etcd{
|
||||||
|
Discovery: "http://disco.example.com/foobar",
|
||||||
|
PeerBindAddr: "127.0.0.1:7002",
|
||||||
|
},
|
||||||
|
[]Unit{{config.Unit{
|
||||||
|
Name: "etcd.service",
|
||||||
|
Runtime: true,
|
||||||
|
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"
|
||||||
|
`,
|
||||||
|
}},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config.Etcd{
|
||||||
|
Name: "node001",
|
||||||
|
Discovery: "http://disco.example.com/foobar",
|
||||||
|
PeerBindAddr: "127.0.0.1:7002",
|
||||||
|
},
|
||||||
|
[]Unit{{config.Unit{
|
||||||
|
Name: "etcd.service",
|
||||||
|
Runtime: true,
|
||||||
|
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 (%+v): want %#v, got %#v", tt.config, tt.units, units)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -17,21 +17,24 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// File is a top-level structure which embeds its underlying configuration,
|
||||||
|
// config.File, and provides the system-specific Permissions().
|
||||||
type File struct {
|
type File struct {
|
||||||
Encoding string
|
config.File
|
||||||
Content string
|
|
||||||
Owner string
|
|
||||||
Path string
|
|
||||||
RawFilePermissions string `yaml:"permissions"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *File) Permissions() (os.FileMode, error) {
|
func (f *File) Permissions() (os.FileMode, error) {
|
||||||
@@ -39,21 +42,73 @@ func (f *File) Permissions() (os.FileMode, error) {
|
|||||||
return os.FileMode(0644), nil
|
return os.FileMode(0644), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse string representation of file mode as octal
|
// Parse string representation of file mode as integer
|
||||||
perm, err := strconv.ParseInt(f.RawFilePermissions, 8, 32)
|
perm, err := strconv.ParseInt(f.RawFilePermissions, 0, 32)
|
||||||
if err != nil {
|
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
|
return os.FileMode(perm), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteFile(f *File, root string) (string, error) {
|
func DecodeBase64Content(content string) ([]byte, error) {
|
||||||
if f.Encoding != "" {
|
output, err := base64.StdEncoding.DecodeString(content)
|
||||||
return "", fmt.Errorf("Unable to write file with encoding %s", f.Encoding)
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to decode base64: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeGzipContent(content string) ([]byte, error) {
|
||||||
|
gzr, err := gzip.NewReader(bytes.NewReader([]byte(content)))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to decode gzip: %v", err)
|
||||||
|
}
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(gzr)
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeContent(content string, encoding string) ([]byte, error) {
|
||||||
|
switch encoding {
|
||||||
|
case "":
|
||||||
|
return []byte(content), nil
|
||||||
|
|
||||||
|
case "b64", "base64":
|
||||||
|
return DecodeBase64Content(content)
|
||||||
|
|
||||||
|
case "gz", "gzip":
|
||||||
|
return DecodeGzipContent(content)
|
||||||
|
|
||||||
|
case "gz+base64", "gzip+base64", "gz+b64", "gzip+b64":
|
||||||
|
gz, err := DecodeBase64Content(content)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return DecodeGzipContent(string(gz))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Unsupported encoding %s", encoding)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteFile(f *File, root string) (string, error) {
|
||||||
fullpath := path.Join(root, f.Path)
|
fullpath := path.Join(root, f.Path)
|
||||||
dir := path.Dir(fullpath)
|
dir := path.Dir(fullpath)
|
||||||
|
log.Printf("Writing file to %q", fullpath)
|
||||||
|
|
||||||
|
content, err := DecodeContent(f.Content, f.Encoding)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Unable to decode %s (%v)", f.Path, err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := EnsureDirectoryExists(dir); err != nil {
|
if err := EnsureDirectoryExists(dir); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -70,7 +125,7 @@ func WriteFile(f *File, root string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(tmp.Name(), []byte(f.Content), perm); err != nil {
|
if err := ioutil.WriteFile(tmp.Name(), content, perm); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +150,7 @@ func WriteFile(f *File, root string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("Wrote file to %q", fullpath)
|
||||||
return fullpath, nil
|
return fullpath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,6 +21,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWriteFileUnencodedContent(t *testing.T) {
|
func TestWriteFileUnencodedContent(t *testing.T) {
|
||||||
@@ -33,11 +35,11 @@ func TestWriteFileUnencodedContent(t *testing.T) {
|
|||||||
fn := "foo"
|
fn := "foo"
|
||||||
fullPath := path.Join(dir, fn)
|
fullPath := path.Join(dir, fn)
|
||||||
|
|
||||||
wf := File{
|
wf := File{config.File{
|
||||||
Path: fn,
|
Path: fn,
|
||||||
Content: "bar",
|
Content: "bar",
|
||||||
RawFilePermissions: "0644",
|
RawFilePermissions: "0644",
|
||||||
}
|
}}
|
||||||
|
|
||||||
path, err := WriteFile(&wf, dir)
|
path, err := WriteFile(&wf, dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -72,17 +74,49 @@ func TestWriteFileInvalidPermission(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
wf := File{
|
wf := File{config.File{
|
||||||
Path: path.Join(dir, "tmp", "foo"),
|
Path: path.Join(dir, "tmp", "foo"),
|
||||||
Content: "bar",
|
Content: "bar",
|
||||||
RawFilePermissions: "pants",
|
RawFilePermissions: "pants",
|
||||||
}
|
}}
|
||||||
|
|
||||||
if _, err := WriteFile(&wf, dir); err == nil {
|
if _, err := WriteFile(&wf, dir); err == nil {
|
||||||
t.Fatalf("Expected error to be raised when writing file with invalid permission")
|
t.Fatalf("Expected error to be raised when writing file with invalid permission")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestWriteFilePermissions(t *testing.T) {
|
||||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -93,10 +127,10 @@ func TestWriteFilePermissions(t *testing.T) {
|
|||||||
fn := "foo"
|
fn := "foo"
|
||||||
fullPath := path.Join(dir, fn)
|
fullPath := path.Join(dir, fn)
|
||||||
|
|
||||||
wf := File{
|
wf := File{config.File{
|
||||||
Path: fn,
|
Path: fn,
|
||||||
RawFilePermissions: "0755",
|
RawFilePermissions: "0755",
|
||||||
}
|
}}
|
||||||
|
|
||||||
path, err := WriteFile(&wf, dir)
|
path, err := WriteFile(&wf, dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,11 +156,98 @@ func TestWriteFileEncodedContent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(dir)
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
wf := File{
|
//all of these decode to "bar"
|
||||||
|
content_tests := map[string]string{
|
||||||
|
"base64": "YmFy",
|
||||||
|
"b64": "YmFy",
|
||||||
|
"gz": "\x1f\x8b\x08\x08w\x14\x87T\x02\xffok\x00KJ,\x02\x00\xaa\x8c\xffv\x03\x00\x00\x00",
|
||||||
|
"gzip": "\x1f\x8b\x08\x08w\x14\x87T\x02\xffok\x00KJ,\x02\x00\xaa\x8c\xffv\x03\x00\x00\x00",
|
||||||
|
"gz+base64": "H4sIABMVh1QAA0tKLAIAqoz/dgMAAAA=",
|
||||||
|
"gzip+base64": "H4sIABMVh1QAA0tKLAIAqoz/dgMAAAA=",
|
||||||
|
"gz+b64": "H4sIABMVh1QAA0tKLAIAqoz/dgMAAAA=",
|
||||||
|
"gzip+b64": "H4sIABMVh1QAA0tKLAIAqoz/dgMAAAA=",
|
||||||
|
}
|
||||||
|
|
||||||
|
for encoding, content := range content_tests {
|
||||||
|
fullPath := path.Join(dir, encoding)
|
||||||
|
|
||||||
|
wf := File{config.File{
|
||||||
|
Path: encoding,
|
||||||
|
Encoding: encoding,
|
||||||
|
Content: content,
|
||||||
|
RawFilePermissions: "0644",
|
||||||
|
}}
|
||||||
|
|
||||||
|
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(0644) {
|
||||||
|
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
contents, err := ioutil.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to read expected file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(contents) != "bar" {
|
||||||
|
t.Fatalf("File has incorrect contents: '%s'", contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFileInvalidEncodedContent(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)
|
||||||
|
|
||||||
|
content_encodings := []string{
|
||||||
|
"base64",
|
||||||
|
"b64",
|
||||||
|
"gz",
|
||||||
|
"gzip",
|
||||||
|
"gz+base64",
|
||||||
|
"gzip+base64",
|
||||||
|
"gz+b64",
|
||||||
|
"gzip+b64",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, encoding := range content_encodings {
|
||||||
|
wf := File{config.File{
|
||||||
|
Path: path.Join(dir, "tmp", "foo"),
|
||||||
|
Content: "@&*#%invalid data*@&^#*&",
|
||||||
|
Encoding: encoding,
|
||||||
|
}}
|
||||||
|
|
||||||
|
if _, err := WriteFile(&wf, dir); err == nil {
|
||||||
|
t.Fatalf("Expected error to be raised when writing file with encoding")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFileUnknownEncodedContent(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)
|
||||||
|
|
||||||
|
wf := File{config.File{
|
||||||
Path: path.Join(dir, "tmp", "foo"),
|
Path: path.Join(dir, "tmp", "foo"),
|
||||||
Content: "",
|
Content: "",
|
||||||
Encoding: "base64",
|
Encoding: "no-such-encoding",
|
||||||
}
|
}}
|
||||||
|
|
||||||
if _, err := WriteFile(&wf, dir); err == nil {
|
if _, err := WriteFile(&wf, dir); err == nil {
|
||||||
t.Fatalf("Expected error to be raised when writing file with encoding")
|
t.Fatalf("Expected error to be raised when writing file with encoding")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
system/fleet.go
Normal file
40
system/fleet.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fleet is a top-level structure which embeds its underlying configuration,
|
||||||
|
// config.Fleet, and provides the system-specific Unit().
|
||||||
|
type Fleet struct {
|
||||||
|
config.Fleet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Units generates a Unit file drop-in for fleet, if any fleet options were
|
||||||
|
// configured in cloud-config
|
||||||
|
func (fe Fleet) Units() []Unit {
|
||||||
|
return []Unit{{config.Unit{
|
||||||
|
Name: "fleet.service",
|
||||||
|
Runtime: true,
|
||||||
|
DropIns: []config.UnitDropIn{{
|
||||||
|
Name: "20-cloudinit.conf",
|
||||||
|
Content: serviceContents(fe.Fleet),
|
||||||
|
}},
|
||||||
|
}}}
|
||||||
|
}
|
60
system/fleet_test.go
Normal file
60
system/fleet_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFleetUnits(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
config config.Fleet
|
||||||
|
units []Unit
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
config.Fleet{},
|
||||||
|
[]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",
|
||||||
|
Runtime: true,
|
||||||
|
DropIns: []config.UnitDropIn{{
|
||||||
|
Name: "20-cloudinit.conf",
|
||||||
|
Content: `[Service]
|
||||||
|
Environment="FLEET_PUBLIC_IP=12.34.56.78"
|
||||||
|
`,
|
||||||
|
}},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
units := Fleet{tt.config}.Units()
|
||||||
|
if !reflect.DeepEqual(units, tt.units) {
|
||||||
|
t.Errorf("bad units (%+v): want %#v, got %#v", tt.config, tt.units, units)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
system/locksmith.go
Normal file
39
system/locksmith.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Locksmith is a top-level structure which embeds its underlying configuration,
|
||||||
|
// config.Locksmith, and provides the system-specific Unit().
|
||||||
|
type Locksmith struct {
|
||||||
|
config.Locksmith
|
||||||
|
}
|
||||||
|
|
||||||
|
// Units creates a Unit file drop-in for etcd, using any configured options.
|
||||||
|
func (ee Locksmith) Units() []Unit {
|
||||||
|
return []Unit{{config.Unit{
|
||||||
|
Name: "locksmithd.service",
|
||||||
|
Runtime: true,
|
||||||
|
DropIns: []config.UnitDropIn{{
|
||||||
|
Name: "20-cloudinit.conf",
|
||||||
|
Content: serviceContents(ee.Locksmith),
|
||||||
|
}},
|
||||||
|
}}}
|
||||||
|
}
|
60
system/locksmith_test.go
Normal file
60
system/locksmith_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLocksmithUnits(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
config config.Locksmith
|
||||||
|
units []Unit
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
config.Locksmith{},
|
||||||
|
[]Unit{{config.Unit{
|
||||||
|
Name: "locksmithd.service",
|
||||||
|
Runtime: true,
|
||||||
|
DropIns: []config.UnitDropIn{{Name: "20-cloudinit.conf"}},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config.Locksmith{
|
||||||
|
Endpoint: "12.34.56.78:4001",
|
||||||
|
},
|
||||||
|
[]Unit{{config.Unit{
|
||||||
|
Name: "locksmithd.service",
|
||||||
|
Runtime: true,
|
||||||
|
DropIns: []config.UnitDropIn{{
|
||||||
|
Name: "20-cloudinit.conf",
|
||||||
|
Content: `[Service]
|
||||||
|
Environment="LOCKSMITHD_ENDPOINT=12.34.56.78:4001"
|
||||||
|
`,
|
||||||
|
}},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
units := Locksmith{tt.config}.Units()
|
||||||
|
if !reflect.DeepEqual(units, tt.units) {
|
||||||
|
t.Errorf("bad units (%+v): want %#v, got %#v", tt.config, tt.units, units)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -23,15 +23,12 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
"github.com/coreos/coreos-cloudinit/network"
|
"github.com/coreos/coreos-cloudinit/network"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/dotcloud/docker/pkg/netlink"
|
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/dotcloud/docker/pkg/netlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
runtimeNetworkPath = "/run/systemd/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
|
func RestartNetwork(interfaces []network.InterfaceGenerator) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if e := restartNetworkd(); e != nil {
|
if e := restartNetworkd(); e != nil {
|
||||||
@@ -95,33 +92,7 @@ func maybeProbeBonding(interfaces []network.InterfaceGenerator) error {
|
|||||||
|
|
||||||
func restartNetworkd() error {
|
func restartNetworkd() error {
|
||||||
log.Printf("Restarting networkd.service\n")
|
log.Printf("Restarting networkd.service\n")
|
||||||
_, err := NewUnitManager("").RunUnitCommand("restart", "systemd-networkd.service")
|
networkd := Unit{config.Unit{Name: "systemd-networkd.service"}}
|
||||||
return err
|
_, err := NewUnitManager("").RunUnitCommand(networkd, "restart")
|
||||||
}
|
|
||||||
|
|
||||||
func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
|
|
||||||
for _, iface := range interfaces {
|
|
||||||
filename := fmt.Sprintf("%s.netdev", iface.Filename())
|
|
||||||
if err := writeConfig(filename, iface.Netdev()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
filename = fmt.Sprintf("%s.link", iface.Filename())
|
|
||||||
if err := writeConfig(filename, iface.Link()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
filename = fmt.Sprintf("%s.network", iface.Filename())
|
|
||||||
if err := writeConfig(filename, iface.Network()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeConfig(filename string, config string) error {
|
|
||||||
if config == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
log.Printf("Writing networkd unit %q\n", filename)
|
|
||||||
_, err := WriteFile(&File{Content: config, Path: filename}, runtimeNetworkPath)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
48
system/oem.go
Normal file
48
system/oem.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 CoreOS, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OEM is a top-level structure which embeds its underlying configuration,
|
||||||
|
// config.OEM, and provides the system-specific File().
|
||||||
|
type OEM struct {
|
||||||
|
config.OEM
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oem OEM) File() (*File, error) {
|
||||||
|
if oem.ID == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
content := fmt.Sprintf("ID=%s\n", oem.ID)
|
||||||
|
content += fmt.Sprintf("VERSION_ID=%s\n", oem.VersionID)
|
||||||
|
content += fmt.Sprintf("NAME=%q\n", oem.Name)
|
||||||
|
content += fmt.Sprintf("HOME_URL=%q\n", oem.HomeURL)
|
||||||
|
content += fmt.Sprintf("BUG_REPORT_URL=%q\n", oem.BugReportURL)
|
||||||
|
|
||||||
|
return &File{config.File{
|
||||||
|
Path: path.Join("etc", "oem-release"),
|
||||||
|
RawFilePermissions: "0644",
|
||||||
|
Content: content,
|
||||||
|
}}, nil
|
||||||
|
}
|
63
system/oem_test.go
Normal file
63
system/oem_test.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOEMFile(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
config config.OEM
|
||||||
|
file *File
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
config.OEM{},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config.OEM{
|
||||||
|
ID: "rackspace",
|
||||||
|
Name: "Rackspace Cloud Servers",
|
||||||
|
VersionID: "168.0.0",
|
||||||
|
HomeURL: "https://www.rackspace.com/cloud/servers/",
|
||||||
|
BugReportURL: "https://github.com/coreos/coreos-overlay",
|
||||||
|
},
|
||||||
|
&File{config.File{
|
||||||
|
Path: "etc/oem-release",
|
||||||
|
RawFilePermissions: "0644",
|
||||||
|
Content: `ID=rackspace
|
||||||
|
VERSION_ID=168.0.0
|
||||||
|
NAME="Rackspace Cloud Servers"
|
||||||
|
HOME_URL="https://www.rackspace.com/cloud/servers/"
|
||||||
|
BUG_REPORT_URL="https://github.com/coreos/coreos-overlay"
|
||||||
|
`,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
file, err := OEM{tt.config}.File()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("bad error (%q): want %v, got %q", tt.config, nil, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(tt.file, file) {
|
||||||
|
t.Errorf("bad file (%q): want %#v, got %#v", tt.config, tt.file, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -23,10 +23,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/coreos/go-systemd/dbus"
|
"github.com/coreos/coreos-cloudinit/Godeps/_workspace/src/github.com/coreos/go-systemd/dbus"
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewUnitManager(root string) UnitManager {
|
func NewUnitManager(root string) UnitManager {
|
||||||
@@ -41,49 +41,51 @@ type systemd struct {
|
|||||||
// never be used as a true MachineID
|
// never be used as a true MachineID
|
||||||
const fakeMachineID = "42000000000000000000000000000042"
|
const fakeMachineID = "42000000000000000000000000000042"
|
||||||
|
|
||||||
// PlaceUnit writes a unit file at the provided destination, creating
|
// PlaceUnit writes a unit file at its desired destination, creating parent
|
||||||
// parent directories as necessary.
|
// directories as necessary.
|
||||||
func (s *systemd) PlaceUnit(u *Unit, dst string) error {
|
func (s *systemd) PlaceUnit(u Unit) error {
|
||||||
dir := filepath.Dir(dst)
|
file := File{config.File{
|
||||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
Path: u.Destination(s.root),
|
||||||
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file := File{
|
|
||||||
Path: filepath.Base(dst),
|
|
||||||
Content: u.Content,
|
Content: u.Content,
|
||||||
RawFilePermissions: "0644",
|
RawFilePermissions: "0644",
|
||||||
}
|
}}
|
||||||
|
|
||||||
_, err := WriteFile(&file, dir)
|
_, err := WriteFile(&file, "/")
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
conn, err := dbus.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
units := []string{unit}
|
units := []string{u.Name}
|
||||||
_, _, err = conn.EnableUnitFiles(units, runtime, true)
|
_, _, err = conn.EnableUnitFiles(units, u.Runtime, true)
|
||||||
return err
|
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()
|
conn, err := dbus.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var fn func(string, string) (string, error)
|
var fn func(string, string) (string, error)
|
||||||
switch command {
|
switch c {
|
||||||
case "start":
|
case "start":
|
||||||
fn = conn.StartUnit
|
fn = conn.StartUnit
|
||||||
case "stop":
|
case "stop":
|
||||||
@@ -99,10 +101,10 @@ func (s *systemd) RunUnitCommand(command, unit string) (string, error) {
|
|||||||
case "reload-or-try-restart":
|
case "reload-or-try-restart":
|
||||||
fn = conn.ReloadOrTryRestartUnit
|
fn = conn.ReloadOrTryRestartUnit
|
||||||
default:
|
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 {
|
func (s *systemd) DaemonReload() error {
|
||||||
@@ -118,8 +120,8 @@ func (s *systemd) DaemonReload() error {
|
|||||||
// /dev/null, analogous to `systemctl mask`.
|
// /dev/null, analogous to `systemctl mask`.
|
||||||
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
|
// N.B.: Unlike `systemctl mask`, this function will *remove any existing unit
|
||||||
// file at the location*, to ensure that the mask will succeed.
|
// file at the location*, to ensure that the mask will succeed.
|
||||||
func (s *systemd) MaskUnit(unit *Unit) error {
|
func (s *systemd) MaskUnit(u Unit) error {
|
||||||
masked := unit.Destination(s.root)
|
masked := u.Destination(s.root)
|
||||||
if _, err := os.Stat(masked); os.IsNotExist(err) {
|
if _, err := os.Stat(masked); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
|
if err := os.MkdirAll(path.Dir(masked), os.FileMode(0755)); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -133,8 +135,8 @@ func (s *systemd) MaskUnit(unit *Unit) error {
|
|||||||
// UnmaskUnit is analogous to systemd's unit_file_unmask. If the file
|
// 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
|
// associated with the given Unit is empty or appears to be a symlink to
|
||||||
// /dev/null, it is removed.
|
// /dev/null, it is removed.
|
||||||
func (s *systemd) UnmaskUnit(unit *Unit) error {
|
func (s *systemd) UnmaskUnit(u Unit) error {
|
||||||
masked := unit.Destination(s.root)
|
masked := u.Destination(s.root)
|
||||||
ne, err := nullOrEmpty(masked)
|
ne, err := nullOrEmpty(masked)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil
|
return nil
|
||||||
|
@@ -17,139 +17,118 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPlaceNetworkUnit(t *testing.T) {
|
func TestPlaceUnit(t *testing.T) {
|
||||||
u := Unit{
|
tests := []config.Unit{
|
||||||
Name: "50-eth0.network",
|
{
|
||||||
Runtime: true,
|
Name: "50-eth0.network",
|
||||||
Content: `[Match]
|
Runtime: true,
|
||||||
Name=eth47
|
Content: "[Match]\nName=eth47\n\n[Network]\nAddress=10.209.171.177/19\n",
|
||||||
|
},
|
||||||
[Network]
|
{
|
||||||
Address=10.209.171.177/19
|
Name: "media-state.mount",
|
||||||
`,
|
Content: "[Mount]\nWhat=/dev/sdb1\nWhere=/media/state\n",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
for _, tt := range tests {
|
||||||
if err != nil {
|
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||||
t.Fatalf("Unable to create tempdir: %v", err)
|
if err != nil {
|
||||||
}
|
panic(fmt.Sprintf("Unable to create tempdir: %v", err))
|
||||||
defer os.RemoveAll(dir)
|
}
|
||||||
|
|
||||||
sd := &systemd{dir}
|
u := Unit{tt}
|
||||||
|
sd := &systemd{dir}
|
||||||
|
|
||||||
dst := u.Destination(dir)
|
if err := sd.PlaceUnit(u); err != nil {
|
||||||
expectDst := path.Join(dir, "run", "systemd", "network", "50-eth0.network")
|
t.Fatalf("PlaceUnit(): bad error (%+v): want nil, got %s", tt, err)
|
||||||
if dst != expectDst {
|
}
|
||||||
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
fi, err := os.Stat(u.Destination(dir))
|
||||||
t.Fatalf("PlaceUnit failed: %v", err)
|
if err != nil {
|
||||||
}
|
t.Fatalf("Stat(): bad error (%+v): want nil, got %s", tt, err)
|
||||||
|
}
|
||||||
|
|
||||||
fi, err := os.Stat(dst)
|
if mode := fi.Mode(); mode != os.FileMode(0644) {
|
||||||
if err != nil {
|
t.Errorf("bad filemode (%+v): want %v, got %v", tt, os.FileMode(0644), mode)
|
||||||
t.Fatalf("Unable to stat file: %v", err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if fi.Mode() != os.FileMode(0644) {
|
c, err := ioutil.ReadFile(u.Destination(dir))
|
||||||
t.Errorf("File has incorrect mode: %v", fi.Mode())
|
if err != nil {
|
||||||
}
|
t.Fatalf("ReadFile(): bad error (%+v): want nil, got %s", tt, err)
|
||||||
|
}
|
||||||
|
|
||||||
contents, err := ioutil.ReadFile(dst)
|
if string(c) != tt.Content {
|
||||||
if err != nil {
|
t.Errorf("bad contents (%+v): want %q, got %q", tt, tt.Content, string(c))
|
||||||
t.Fatalf("Unable to read expected file: %v", err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
expectContents := `[Match]
|
os.RemoveAll(dir)
|
||||||
Name=eth47
|
|
||||||
|
|
||||||
[Network]
|
|
||||||
Address=10.209.171.177/19
|
|
||||||
`
|
|
||||||
if string(contents) != expectContents {
|
|
||||||
t.Fatalf("File has incorrect contents '%s'.\nExpected '%s'", string(contents), expectContents)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnitDestination(t *testing.T) {
|
func TestPlaceUnitDropIn(t *testing.T) {
|
||||||
dir := "/some/dir"
|
tests := []config.Unit{
|
||||||
name := "foobar.service"
|
{
|
||||||
|
Name: "false.service",
|
||||||
u := Unit{
|
Runtime: true,
|
||||||
Name: name,
|
DropIns: []config.UnitDropIn{
|
||||||
DropIn: false,
|
{
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
dst := u.Destination(dir)
|
for _, tt := range tests {
|
||||||
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
|
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||||
if dst != expectDst {
|
if err != nil {
|
||||||
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
|
panic(fmt.Sprintf("Unable to create tempdir: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
u.DropIn = true
|
u := Unit{tt}
|
||||||
|
sd := &systemd{dir}
|
||||||
|
|
||||||
dst = u.Destination(dir)
|
if err := sd.PlaceUnitDropIn(u, u.DropIns[0]); err != nil {
|
||||||
expectDst = path.Join(dir, "etc", "systemd", "system", "foobar.service.d", cloudConfigDropIn)
|
t.Fatalf("PlaceUnit(): bad error (%+v): want nil, got %s", tt, err)
|
||||||
if dst != expectDst {
|
}
|
||||||
t.Errorf("unit.Destination returned %s, expected %s", dst, expectDst)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPlaceMountUnit(t *testing.T) {
|
fi, err := os.Stat(u.DropInDestination(dir, u.DropIns[0]))
|
||||||
u := Unit{
|
if err != nil {
|
||||||
Name: "media-state.mount",
|
t.Fatalf("Stat(): bad error (%+v): want nil, got %s", tt, err)
|
||||||
Runtime: false,
|
}
|
||||||
Content: `[Mount]
|
|
||||||
What=/dev/sdb1
|
|
||||||
Where=/media/state
|
|
||||||
`,
|
|
||||||
}
|
|
||||||
|
|
||||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
if mode := fi.Mode(); mode != os.FileMode(0644) {
|
||||||
if err != nil {
|
t.Errorf("bad filemode (%+v): want %v, got %v", tt, os.FileMode(0644), mode)
|
||||||
t.Fatalf("Unable to create tempdir: %v", err)
|
}
|
||||||
}
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
|
|
||||||
sd := &systemd{dir}
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
dst := u.Destination(dir)
|
if string(c) != u.DropIns[0].Content {
|
||||||
expectDst := path.Join(dir, "etc", "systemd", "system", "media-state.mount")
|
t.Errorf("bad contents (%+v): want %q, got %q", tt, u.DropIns[0].Content, string(c))
|
||||||
if dst != expectDst {
|
}
|
||||||
t.Fatalf("unit.Destination returned %s, expected %s", dst, expectDst)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sd.PlaceUnit(&u, dst); err != nil {
|
os.RemoveAll(dir)
|
||||||
t.Fatalf("PlaceUnit failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +157,7 @@ func TestMaskUnit(t *testing.T) {
|
|||||||
sd := &systemd{dir}
|
sd := &systemd{dir}
|
||||||
|
|
||||||
// Ensure mask works with units that do not currently exist
|
// Ensure mask works with units that do not currently exist
|
||||||
uf := &Unit{Name: "foo.service"}
|
uf := Unit{config.Unit{Name: "foo.service"}}
|
||||||
if err := sd.MaskUnit(uf); err != nil {
|
if err := sd.MaskUnit(uf); err != nil {
|
||||||
t.Fatalf("Unable to mask new unit: %v", err)
|
t.Fatalf("Unable to mask new unit: %v", err)
|
||||||
}
|
}
|
||||||
@@ -192,7 +171,7 @@ func TestMaskUnit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure mask works with unit files that already exist
|
// Ensure mask works with unit files that already exist
|
||||||
ub := &Unit{Name: "bar.service"}
|
ub := Unit{config.Unit{Name: "bar.service"}}
|
||||||
barPath := path.Join(dir, "etc", "systemd", "system", "bar.service")
|
barPath := path.Join(dir, "etc", "systemd", "system", "bar.service")
|
||||||
if _, err := os.Create(barPath); err != nil {
|
if _, err := os.Create(barPath); err != nil {
|
||||||
t.Fatalf("Error creating new unit file: %v", err)
|
t.Fatalf("Error creating new unit file: %v", err)
|
||||||
@@ -218,12 +197,12 @@ func TestUnmaskUnit(t *testing.T) {
|
|||||||
|
|
||||||
sd := &systemd{dir}
|
sd := &systemd{dir}
|
||||||
|
|
||||||
nilUnit := &Unit{Name: "null.service"}
|
nilUnit := Unit{config.Unit{Name: "null.service"}}
|
||||||
if err := sd.UnmaskUnit(nilUnit); err != nil {
|
if err := sd.UnmaskUnit(nilUnit); err != nil {
|
||||||
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
|
t.Errorf("unexpected error from unmasking nonexistent unit: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uf := &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)
|
dst := uf.Destination(dir)
|
||||||
if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil {
|
if err := os.MkdirAll(path.Dir(dst), os.FileMode(0755)); err != nil {
|
||||||
t.Fatalf("Unable to create unit directory: %v", err)
|
t.Fatalf("Unable to create unit directory: %v", err)
|
||||||
@@ -243,7 +222,7 @@ func TestUnmaskUnit(t *testing.T) {
|
|||||||
t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly")
|
t.Errorf("unmask of non-empty unit mutated unit contents unexpectedly")
|
||||||
}
|
}
|
||||||
|
|
||||||
ub := &Unit{Name: "bar.service"}
|
ub := Unit{config.Unit{Name: "bar.service"}}
|
||||||
dst = ub.Destination(dir)
|
dst = ub.Destination(dir)
|
||||||
if err := os.Symlink("/dev/null", dst); err != nil {
|
if err := os.Symlink("/dev/null", dst); err != nil {
|
||||||
t.Fatalf("Unable to create masked unit: %v", err)
|
t.Fatalf("Unable to create masked unit: %v", err)
|
||||||
|
@@ -21,63 +21,64 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"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 {
|
type UnitManager interface {
|
||||||
PlaceUnit(unit *Unit, dst string) error
|
PlaceUnit(unit Unit) error
|
||||||
EnableUnitFile(unit string, runtime bool) error
|
PlaceUnitDropIn(unit Unit, dropIn config.UnitDropIn) error
|
||||||
RunUnitCommand(command, unit string) (string, error)
|
EnableUnitFile(unit Unit) error
|
||||||
|
RunUnitCommand(unit Unit, command string) (string, error)
|
||||||
|
MaskUnit(unit Unit) error
|
||||||
|
UnmaskUnit(unit Unit) error
|
||||||
DaemonReload() 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(), Type(), and
|
||||||
|
// Group().
|
||||||
type Unit struct {
|
type Unit struct {
|
||||||
Name string
|
config.Unit
|
||||||
Mask bool
|
|
||||||
Enable bool
|
|
||||||
Runtime bool
|
|
||||||
Content string
|
|
||||||
Command string
|
|
||||||
|
|
||||||
// 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 {
|
// Type returns the extension of the unit (everything that follows the final
|
||||||
|
// period).
|
||||||
|
func (u Unit) Type() string {
|
||||||
ext := filepath.Ext(u.Name)
|
ext := filepath.Ext(u.Name)
|
||||||
return strings.TrimLeft(ext, ".")
|
return strings.TrimLeft(ext, ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unit) Group() (group string) {
|
// Group returns "network" or "system" depending on whether or not the unit is
|
||||||
t := u.Type()
|
// a network unit or otherwise.
|
||||||
if t == "network" || t == "netdev" || t == "link" {
|
func (u Unit) Group() string {
|
||||||
group = "network"
|
switch u.Type() {
|
||||||
} else {
|
case "network", "netdev", "link":
|
||||||
group = "system"
|
return "network"
|
||||||
|
default:
|
||||||
|
return "system"
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Script []byte
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
// Destination builds the appropriate absolute file path for
|
// DropInDestination builds the appropriate absolute file path for the
|
||||||
// the Unit. The root argument indicates the effective base
|
// UnitDropIn. The root argument indicates the effective base directory of the
|
||||||
// directory of the system (similar to a chroot).
|
// system (similar to a chroot) and the dropIn argument is the UnitDropIn for
|
||||||
func (u *Unit) Destination(root string) string {
|
// 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"
|
dir := "etc"
|
||||||
if u.Runtime {
|
if u.Runtime {
|
||||||
dir = "run"
|
dir = "run"
|
||||||
}
|
}
|
||||||
|
return path.Join(root, dir, "systemd", u.Group())
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
153
system/update.go
Normal file
153
system/update.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
locksmithUnit = "locksmithd.service"
|
||||||
|
updateEngineUnit = "update-engine.service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update is a top-level structure which contains its underlying configuration,
|
||||||
|
// config.Update, a function for reading the configuration (the default
|
||||||
|
// implementation reading from the filesystem), and provides the system-specific
|
||||||
|
// File() and Unit().
|
||||||
|
type Update struct {
|
||||||
|
ReadConfig func() (io.Reader, error)
|
||||||
|
config.Update
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultReadConfig() (io.Reader, error) {
|
||||||
|
etcUpdate := path.Join("/etc", "coreos", "update.conf")
|
||||||
|
usrUpdate := path.Join("/usr", "share", "coreos", "update.conf")
|
||||||
|
|
||||||
|
f, err := os.Open(etcUpdate)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
f, err = os.Open(usrUpdate)
|
||||||
|
}
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// File generates an `/etc/coreos/update.conf` file (if any update
|
||||||
|
// configuration options are set in cloud-config) by either rewriting the
|
||||||
|
// existing file on disk, or starting from `/usr/share/coreos/update.conf`
|
||||||
|
func (uc Update) File() (*File, error) {
|
||||||
|
if config.IsZero(uc.Update) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err := config.AssertStructValid(uc.Update); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the list of possible substitutions to be performed based on the options that are configured
|
||||||
|
subs := map[string]string{}
|
||||||
|
uct := reflect.TypeOf(uc.Update)
|
||||||
|
ucv := reflect.ValueOf(uc.Update)
|
||||||
|
for i := 0; i < uct.NumField(); i++ {
|
||||||
|
val := ucv.Field(i).String()
|
||||||
|
if val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
env := uct.Field(i).Tag.Get("env")
|
||||||
|
subs[env] = fmt.Sprintf("%s=%s", env, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf, err := uc.ReadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(conf)
|
||||||
|
|
||||||
|
var out string
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
for env, value := range subs {
|
||||||
|
if strings.HasPrefix(line, env) {
|
||||||
|
line = value
|
||||||
|
delete(subs, env)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out += line
|
||||||
|
out += "\n"
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range sortedKeys(subs) {
|
||||||
|
out += subs[key]
|
||||||
|
out += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &File{config.File{
|
||||||
|
Path: path.Join("etc", "coreos", "update.conf"),
|
||||||
|
RawFilePermissions: "0644",
|
||||||
|
Content: out,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Units generates units for the cloud-init initializer to act on:
|
||||||
|
// - a locksmith Unit, if "reboot-strategy" was set in cloud-config
|
||||||
|
// - an update_engine Unit, if "group" or "server" was set in cloud-config
|
||||||
|
func (uc Update) Units() []Unit {
|
||||||
|
var units []Unit
|
||||||
|
if uc.Update.RebootStrategy != "" {
|
||||||
|
ls := &Unit{config.Unit{
|
||||||
|
Name: locksmithUnit,
|
||||||
|
Command: "restart",
|
||||||
|
Mask: false,
|
||||||
|
Runtime: true,
|
||||||
|
}}
|
||||||
|
|
||||||
|
if uc.Update.RebootStrategy == "false" || uc.Update.RebootStrategy == "off" {
|
||||||
|
ls.Command = "stop"
|
||||||
|
ls.Mask = true
|
||||||
|
}
|
||||||
|
units = append(units, *ls)
|
||||||
|
}
|
||||||
|
|
||||||
|
if uc.Update.Group != "" || uc.Update.Server != "" {
|
||||||
|
ue := Unit{config.Unit{
|
||||||
|
Name: updateEngineUnit,
|
||||||
|
Command: "restart",
|
||||||
|
}}
|
||||||
|
units = append(units, ue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return units
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedKeys(m map[string]string) (keys []string) {
|
||||||
|
for key := range m {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
return
|
||||||
|
}
|
180
system/update_test.go
Normal file
180
system/update_test.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testReadConfig(config string) func() (io.Reader, error) {
|
||||||
|
return func() (io.Reader, error) {
|
||||||
|
return strings.NewReader(config), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateUnits(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
config config.Update
|
||||||
|
units []Unit
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
config: config.Update{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: config.Update{Group: "master", Server: "http://foo.com"},
|
||||||
|
units: []Unit{{config.Unit{
|
||||||
|
Name: "update-engine.service",
|
||||||
|
Command: "restart",
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: config.Update{RebootStrategy: "best-effort"},
|
||||||
|
units: []Unit{{config.Unit{
|
||||||
|
Name: "locksmithd.service",
|
||||||
|
Command: "restart",
|
||||||
|
Runtime: true,
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: config.Update{RebootStrategy: "etcd-lock"},
|
||||||
|
units: []Unit{{config.Unit{
|
||||||
|
Name: "locksmithd.service",
|
||||||
|
Command: "restart",
|
||||||
|
Runtime: true,
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: config.Update{RebootStrategy: "reboot"},
|
||||||
|
units: []Unit{{config.Unit{
|
||||||
|
Name: "locksmithd.service",
|
||||||
|
Command: "restart",
|
||||||
|
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{
|
||||||
|
Name: "locksmithd.service",
|
||||||
|
Command: "stop",
|
||||||
|
Runtime: true,
|
||||||
|
Mask: true,
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
units := Update{Update: tt.config, ReadConfig: testReadConfig("")}.Units()
|
||||||
|
if !reflect.DeepEqual(tt.units, units) {
|
||||||
|
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.units, units)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateFile(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
config config.Update
|
||||||
|
orig string
|
||||||
|
file *File
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
config: config.Update{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: config.Update{RebootStrategy: "wizzlewazzle"},
|
||||||
|
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"},
|
||||||
|
file: &File{config.File{
|
||||||
|
Content: "GROUP=master\nSERVER=http://foo.com\n",
|
||||||
|
Path: "etc/coreos/update.conf",
|
||||||
|
RawFilePermissions: "0644",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: config.Update{RebootStrategy: "best-effort"},
|
||||||
|
file: &File{config.File{
|
||||||
|
Content: "REBOOT_STRATEGY=best-effort\n",
|
||||||
|
Path: "etc/coreos/update.conf",
|
||||||
|
RawFilePermissions: "0644",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: config.Update{RebootStrategy: "etcd-lock"},
|
||||||
|
file: &File{config.File{
|
||||||
|
Content: "REBOOT_STRATEGY=etcd-lock\n",
|
||||||
|
Path: "etc/coreos/update.conf",
|
||||||
|
RawFilePermissions: "0644",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: config.Update{RebootStrategy: "reboot"},
|
||||||
|
file: &File{config.File{
|
||||||
|
Content: "REBOOT_STRATEGY=reboot\n",
|
||||||
|
Path: "etc/coreos/update.conf",
|
||||||
|
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{
|
||||||
|
Content: "REBOOT_STRATEGY=off\n",
|
||||||
|
Path: "etc/coreos/update.conf",
|
||||||
|
RawFilePermissions: "0644",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: config.Update{RebootStrategy: "etcd-lock"},
|
||||||
|
orig: "SERVER=https://example.com\nGROUP=thegroupc\nREBOOT_STRATEGY=awesome",
|
||||||
|
file: &File{config.File{
|
||||||
|
Content: "SERVER=https://example.com\nGROUP=thegroupc\nREBOOT_STRATEGY=etcd-lock\n",
|
||||||
|
Path: "etc/coreos/update.conf",
|
||||||
|
RawFilePermissions: "0644",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
file, err := Update{Update: tt.config, ReadConfig: testReadConfig(tt.orig)}.File()
|
||||||
|
if !reflect.DeepEqual(tt.err, err) {
|
||||||
|
t.Errorf("bad error (%q): want %q, got %q", tt.config, tt.err, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(tt.file, file) {
|
||||||
|
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.file, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -22,30 +22,16 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
func UserExists(u *config.User) bool {
|
||||||
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"`
|
|
||||||
GECOS string `yaml:"gecos"`
|
|
||||||
Homedir string `yaml:"homedir"`
|
|
||||||
NoCreateHome bool `yaml:"no-create-home"`
|
|
||||||
PrimaryGroup string `yaml:"primary-group"`
|
|
||||||
Groups []string `yaml:"groups"`
|
|
||||||
NoUserGroup bool `yaml:"no-user-group"`
|
|
||||||
System bool `yaml:"system"`
|
|
||||||
NoLogInit bool `yaml:"no-log-init"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func UserExists(u *User) bool {
|
|
||||||
_, err := user.Lookup(u.Name)
|
_, err := user.Lookup(u.Name)
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateUser(u *User) error {
|
func CreateUser(u *config.User) error {
|
||||||
args := []string{}
|
args := []string{}
|
||||||
|
|
||||||
if u.PasswordHash != "" {
|
if u.PasswordHash != "" {
|
||||||
|
Reference in New Issue
Block a user