Merge pull request #234 from crawford/validate
config: explicitly specify fields and seperate config and application
This commit is contained in:
commit
cdfc94f4e9
198
config/config.go
Normal file
198
config/config.go
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/third_party/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"`
|
||||||
|
Fleet Fleet `yaml:"fleet"`
|
||||||
|
OEM OEM `yaml:"oem"`
|
||||||
|
Update Update `yaml:"update"`
|
||||||
|
Units []Unit `yaml:"units"`
|
||||||
|
} `yaml:"coreos"`
|
||||||
|
WriteFiles []File `yaml:"write_files"`
|
||||||
|
Hostname string `yaml:"hostname"`
|
||||||
|
Users []User `yaml:"users"`
|
||||||
|
ManageEtcHosts EtcHosts `yaml:"manage_etc_hosts"`
|
||||||
|
NetworkConfigPath string `yaml:"-"`
|
||||||
|
NetworkConfig string `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertValid checks the fields in the structure and makes sure that they
|
||||||
|
// contain valid values as specified by the 'valid' flag. Empty fields are
|
||||||
|
// implicitly valid.
|
||||||
|
func AssertValid(c interface{}) error {
|
||||||
|
ct := reflect.TypeOf(c)
|
||||||
|
cv := reflect.ValueOf(c)
|
||||||
|
for i := 0; i < ct.NumField(); i++ {
|
||||||
|
ft := ct.Field(i)
|
||||||
|
if !isFieldExported(ft) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
valid := ft.Tag.Get("valid")
|
||||||
|
val := cv.Field(i)
|
||||||
|
if !isValid(val, valid) {
|
||||||
|
return fmt.Errorf("invalid value \"%v\" for option %q (valid options: %q)", val.Interface(), ft.Name, valid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 isValid(v reflect.Value, valid string) bool {
|
||||||
|
if valid == "" || isZero(v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
vs := fmt.Sprintf("%v", v.Interface())
|
||||||
|
for _, valid := range strings.Split(valid, ",") {
|
||||||
|
if vs == valid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
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(&User{})
|
||||||
|
yaml.Unmarshal(b, &known)
|
||||||
|
|
||||||
|
if set, ok := users.([]interface{}); ok {
|
||||||
|
for _, u := range set {
|
||||||
|
if user, ok := u.(map[interface{}]interface{}); ok {
|
||||||
|
for k, _ := range user {
|
||||||
|
if key, ok := k.(string); ok {
|
||||||
|
if _, ok := known[key]; !ok {
|
||||||
|
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn("Warning: unrecognized key %q in user section of cloud config - ignoring", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any badly-specified files, if any are set
|
||||||
|
if files, ok := c["write_files"]; ok {
|
||||||
|
var known map[string]interface{}
|
||||||
|
b, _ := yaml.Marshal(&File{})
|
||||||
|
yaml.Unmarshal(b, &known)
|
||||||
|
|
||||||
|
if set, ok := files.([]interface{}); ok {
|
||||||
|
for _, f := range set {
|
||||||
|
if file, ok := f.(map[interface{}]interface{}); ok {
|
||||||
|
for k, _ := range file {
|
||||||
|
if key, ok := k.(string); ok {
|
||||||
|
if _, ok := known[key]; !ok {
|
||||||
|
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn("Warning: unrecognized key %q in file section of cloud config - ignoring", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
473
config/config_test.go
Normal file
473
config/config_test.go
Normal file
@ -0,0 +1,473 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"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 %q, got %q", tt.c, tt.empty, empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAssertValid(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"}, errors.New("invalid value \"hello\" for option \"A\" (valid options: \"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}, errors.New("invalid value \"9\" for option \"A\" (valid options: \"1,2\")")},
|
||||||
|
} {
|
||||||
|
if err := AssertValid(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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 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.RebootStrategy != "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", 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", 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", 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)
|
||||||
|
}
|
||||||
|
}
|
3
config/etc_hosts.go
Normal file
3
config/etc_hosts.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type EtcHosts string
|
32
config/etcd.go
Normal file
32
config/etcd.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Etcd struct {
|
||||||
|
Addr string `yaml:"addr" env:"ETCD_ADDR"`
|
||||||
|
BindAddr string `yaml:"bind-addr" env:"ETCD_BIND_ADDR"`
|
||||||
|
CAFile string `yaml:"ca-file" env:"ETCD_CA_FILE"`
|
||||||
|
CertFile string `yaml:"cert-file" env:"ETCD_CERT_FILE"`
|
||||||
|
ClusterActiveSize string `yaml:"cluster-active-size" env:"ETCD_CLUSTER_ACTIVE_SIZE"`
|
||||||
|
ClusterRemoveDelay string `yaml:"cluster-remove-delay" env:"ETCD_CLUSTER_REMOVE_DELAY"`
|
||||||
|
ClusterSyncInterval string `yaml:"cluster-sync-interval" env:"ETCD_CLUSTER_SYNC_INTERVAL"`
|
||||||
|
Cors string `yaml:"cors" env:"ETCD_CORS"`
|
||||||
|
CPUProfileFile string `yaml:"cpu-profile-file" env:"ETCD_CPU_PROFILE_FILE"`
|
||||||
|
DataDir string `yaml:"data-dir" env:"ETCD_DATA_DIR"`
|
||||||
|
Discovery string `yaml:"discovery" env:"ETCD_DISCOVERY"`
|
||||||
|
HTTPReadTimeout string `yaml:"http-read-timeout" env:"ETCD_HTTP_READ_TIMEOUT"`
|
||||||
|
HTTPWriteTimeout string `yaml:"http-write-timeout" env:"ETCD_HTTP_WRITE_TIMEOUT"`
|
||||||
|
KeyFile string `yaml:"key-file" env:"ETCD_KEY_FILE"`
|
||||||
|
MaxClusterSize string `yaml:"max-cluster-size" env:"ETCD_MAX_CLUSTER_SIZE"`
|
||||||
|
MaxResultBuffer string `yaml:"max-result-buffer" env:"ETCD_MAX_RESULT_BUFFER"`
|
||||||
|
MaxRetryAttempts string `yaml:"max-retry-attempts" env:"ETCD_MAX_RETRY_ATTEMPTS"`
|
||||||
|
Name string `yaml:"name" env:"ETCD_NAME"`
|
||||||
|
PeerAddr string `yaml:"peer-addr" env:"ETCD_PEER_ADDR"`
|
||||||
|
PeerBindAddr string `yaml:"peer-bind-addr" env:"ETCD_PEER_BIND_ADDR"`
|
||||||
|
PeerCAFile string `yaml:"peer-ca-file" env:"ETCD_PEER_CA_FILE"`
|
||||||
|
PeerCertFile string `yaml:"peer-cert-file" env:"ETCD_PEER_CERT_FILE"`
|
||||||
|
PeerKeyFile string `yaml:"peer-key-file" env:"ETCD_PEER_KEY_FILE"`
|
||||||
|
Peers string `yaml:"peers" env:"ETCD_PEERS"`
|
||||||
|
PeersFile string `yaml:"peers-file" env:"ETCD_PEERS_FILE"`
|
||||||
|
Snapshot string `yaml:"snapshot" env:"ETCD_SNAPSHOT"`
|
||||||
|
Verbose string `yaml:"verbose" env:"ETCD_VERBOSE"`
|
||||||
|
VeryVerbose string `yaml:"very-verbose" env:"ETCD_VERY_VERBOSE"`
|
||||||
|
}
|
9
config/file.go
Normal file
9
config/file.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Encoding string `yaml:"-"`
|
||||||
|
Content string `yaml:"content"`
|
||||||
|
Owner string `yaml:"owner"`
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
RawFilePermissions string `yaml:"permissions"`
|
||||||
|
}
|
14
config/fleet.go
Normal file
14
config/fleet.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Fleet struct {
|
||||||
|
AgentTTL string `yaml:"agent-ttl" env:"FLEET_AGENT_TTL"`
|
||||||
|
EngineReconcileInterval string `yaml:"engine-reconcile-interval" env:"FLEET_ENGINE_RECONCILE_INTERVAL"`
|
||||||
|
EtcdCAFile string `yaml:"etcd-cafile" env:"FLEET_ETCD_CAFILE"`
|
||||||
|
EtcdCertFile string `yaml:"etcd-certfile" env:"FLEET_ETCD_CERTFILE"`
|
||||||
|
EtcdKeyFile string `yaml:"etcd-keyfile" env:"FLEET_ETCD_KEYFILE"`
|
||||||
|
EtcdRequestTimeout string `yaml:"etcd-request-timeout" env:"FLEET_ETCD_REQUEST_TIMEOUT"`
|
||||||
|
EtcdServers string `yaml:"etcd-servers" env:"FLEET_ETCD_SERVERS"`
|
||||||
|
Metadata string `yaml:"metadata" env:"FLEET_METADATA"`
|
||||||
|
PublicIP string `yaml:"public-ip" env:"FLEET_PUBLIC_IP"`
|
||||||
|
Verbosity string `yaml:"verbosity" env:"FLEET_VERBOSITY"`
|
||||||
|
}
|
9
config/oem.go
Normal file
9
config/oem.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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"`
|
||||||
|
}
|
34
config/unit.go
Normal file
34
config/unit.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Unit struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Mask bool `yaml:"mask"`
|
||||||
|
Enable bool `yaml:"enable"`
|
||||||
|
Runtime bool `yaml:"runtime"`
|
||||||
|
Content string `yaml:"content"`
|
||||||
|
Command string `yaml:"command"`
|
||||||
|
|
||||||
|
// For drop-in units, a cloudinit.conf is generated.
|
||||||
|
// This is currently unbound in YAML (and hence unsettable in cloud-config files)
|
||||||
|
// until the correct behaviour for multiple drop-in units is determined.
|
||||||
|
DropIn bool `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Unit) Type() string {
|
||||||
|
ext := filepath.Ext(u.Name)
|
||||||
|
return strings.TrimLeft(ext, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Unit) Group() string {
|
||||||
|
switch u.Type() {
|
||||||
|
case "network", "netdev", "link":
|
||||||
|
return "network"
|
||||||
|
default:
|
||||||
|
return "system"
|
||||||
|
}
|
||||||
|
}
|
7
config/update.go
Normal file
7
config/update.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
RebootStrategy string `yaml:"reboot-strategy" env:"REBOOT_STRATEGY" valid:"best-effort,etcd-lock,reboot,off"`
|
||||||
|
Group string `yaml:"group" env:"GROUP"`
|
||||||
|
Server string `yaml:"server" env:"SERVER"`
|
||||||
|
}
|
17
config/user.go
Normal file
17
config/user.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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"`
|
||||||
|
}
|
@ -7,6 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
"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"
|
||||||
@ -156,7 +157,7 @@ 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 *system.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)
|
||||||
@ -178,14 +179,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 system.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)
|
||||||
@ -224,7 +225,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)
|
||||||
|
@ -4,37 +4,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"}`,
|
||||||
@ -43,17 +43,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",
|
||||||
@ -62,15 +62,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",
|
||||||
@ -79,33 +79,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"}`,
|
||||||
|
@ -6,8 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/coreos/coreos-cloudinit/third_party/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"
|
||||||
)
|
)
|
||||||
@ -17,146 +16,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, error)
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
@ -216,26 +88,44 @@ 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})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ccf := range []CloudConfigFile{
|
||||||
|
system.OEM{cfg.Coreos.OEM},
|
||||||
|
system.Update{cfg.Coreos.Update, system.DefaultReadConfig},
|
||||||
|
system.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 {
|
||||||
|
units = append(units, system.Unit{u})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ccu := range []CloudConfigUnit{
|
||||||
|
system.Etcd{cfg.Coreos.Etcd},
|
||||||
|
system.Fleet{cfg.Coreos.Fleet},
|
||||||
|
system.Update{cfg.Coreos.Update, system.DefaultReadConfig},
|
||||||
|
} {
|
||||||
|
u, err := ccu.Units()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cfg.Coreos.Units = append(cfg.Coreos.Units, u...)
|
units = append(units, u...)
|
||||||
}
|
}
|
||||||
|
|
||||||
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,7 +172,7 @@ func Apply(cfg CloudConfig, env *Environment) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
um := system.NewUnitManager(env.Root())
|
um := system.NewUnitManager(env.Root())
|
||||||
return processUnits(cfg.Coreos.Units, env.Root(), um)
|
return processUnits(units, env.Root(), um)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,368 +1,12 @@
|
|||||||
package initialize
|
package initialize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
"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", 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
|
||||||
@ -401,10 +45,10 @@ func (tum *TestUnitManager) UnmaskUnit(unit *system.Unit) error {
|
|||||||
func TestProcessUnits(t *testing.T) {
|
func TestProcessUnits(t *testing.T) {
|
||||||
tum := &TestUnitManager{}
|
tum := &TestUnitManager{}
|
||||||
units := []system.Unit{
|
units := []system.Unit{
|
||||||
system.Unit{
|
system.Unit{config.Unit{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Mask: true,
|
Mask: true,
|
||||||
},
|
}},
|
||||||
}
|
}
|
||||||
if err := processUnits(units, "", tum); err != nil {
|
if err := processUnits(units, "", tum); err != nil {
|
||||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||||
@ -415,9 +59,9 @@ func TestProcessUnits(t *testing.T) {
|
|||||||
|
|
||||||
tum = &TestUnitManager{}
|
tum = &TestUnitManager{}
|
||||||
units = []system.Unit{
|
units = []system.Unit{
|
||||||
system.Unit{
|
system.Unit{config.Unit{
|
||||||
Name: "bar.network",
|
Name: "bar.network",
|
||||||
},
|
}},
|
||||||
}
|
}
|
||||||
if err := processUnits(units, "", tum); err != nil {
|
if err := processUnits(units, "", tum); err != nil {
|
||||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||||
@ -428,10 +72,10 @@ func TestProcessUnits(t *testing.T) {
|
|||||||
|
|
||||||
tum = &TestUnitManager{}
|
tum = &TestUnitManager{}
|
||||||
units = []system.Unit{
|
units = []system.Unit{
|
||||||
system.Unit{
|
system.Unit{config.Unit{
|
||||||
Name: "baz.service",
|
Name: "baz.service",
|
||||||
Content: "[Service]\nExecStart=/bin/true",
|
Content: "[Service]\nExecStart=/bin/true",
|
||||||
},
|
}},
|
||||||
}
|
}
|
||||||
if err := processUnits(units, "", tum); err != nil {
|
if err := processUnits(units, "", tum); err != nil {
|
||||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||||
@ -442,10 +86,10 @@ func TestProcessUnits(t *testing.T) {
|
|||||||
|
|
||||||
tum = &TestUnitManager{}
|
tum = &TestUnitManager{}
|
||||||
units = []system.Unit{
|
units = []system.Unit{
|
||||||
system.Unit{
|
system.Unit{config.Unit{
|
||||||
Name: "locksmithd.service",
|
Name: "locksmithd.service",
|
||||||
Runtime: true,
|
Runtime: true,
|
||||||
},
|
}},
|
||||||
}
|
}
|
||||||
if err := processUnits(units, "", tum); err != nil {
|
if err := processUnits(units, "", tum); err != nil {
|
||||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||||
@ -456,10 +100,10 @@ func TestProcessUnits(t *testing.T) {
|
|||||||
|
|
||||||
tum = &TestUnitManager{}
|
tum = &TestUnitManager{}
|
||||||
units = []system.Unit{
|
units = []system.Unit{
|
||||||
system.Unit{
|
system.Unit{config.Unit{
|
||||||
Name: "woof",
|
Name: "woof",
|
||||||
Enable: true,
|
Enable: true,
|
||||||
},
|
}},
|
||||||
}
|
}
|
||||||
if err := processUnits(units, "", tum); err != nil {
|
if err := processUnits(units, "", tum); err != nil {
|
||||||
t.Fatalf("unexpected error calling processUnits: %v", err)
|
t.Fatalf("unexpected error calling processUnits: %v", err)
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -81,9 +82,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{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 {
|
||||||
@ -104,16 +105,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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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", 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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,11 +3,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
|
||||||
}
|
}
|
||||||
@ -22,7 +24,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) {
|
||||||
|
@ -1,21 +1,25 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -39,31 +39,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", 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ func ParseUserData(contents string) (interface{}, error) {
|
|||||||
return system.Script(contents), nil
|
return system.Script(contents), nil
|
||||||
} else if header == "#cloud-config" {
|
} else if header == "#cloud-config" {
|
||||||
log.Printf("Parsing user-data as cloud-config")
|
log.Printf("Parsing user-data as cloud-config")
|
||||||
return NewCloudConfig(contents)
|
return config.NewCloudConfig(contents)
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
|
return nil, fmt.Errorf("Unrecognized user-data header: %s", header)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ package initialize
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseHeaderCRLF(t *testing.T) {
|
func TestParseHeaderCRLF(t *testing.T) {
|
||||||
@ -37,7 +39,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")
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
"github.com/coreos/coreos-cloudinit/system"
|
"github.com/coreos/coreos-cloudinit/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,21 +32,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{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{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
|
||||||
}
|
}
|
||||||
|
26
system/env.go
Normal file
26
system/env.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dropinContents generates the contents for a drop-in unit given the config.
|
||||||
|
// The argument must be a struct from the 'config' package.
|
||||||
|
func dropinContents(e interface{}) string {
|
||||||
|
et := reflect.TypeOf(e)
|
||||||
|
ev := reflect.ValueOf(e)
|
||||||
|
|
||||||
|
var out string
|
||||||
|
for i := 0; i < et.NumField(); i++ {
|
||||||
|
if val := ev.Field(i).String(); val != "" {
|
||||||
|
key := et.Field(i).Tag.Get("env")
|
||||||
|
out += fmt.Sprintf("Environment=\"%s=%s\"\n", key, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if out == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "[Service]\n" + out
|
||||||
|
}
|
@ -7,6 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -48,9 +50,9 @@ func TestWriteEnvFileUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueUpdate,
|
Vars: valueUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,9 +97,9 @@ func TestWriteEnvFileUpdateNoNewline(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueUpdate,
|
Vars: valueUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +138,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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,9 +176,9 @@ func TestWriteEnvFileNoop(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueNoop,
|
Vars: valueNoop,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,9 +223,9 @@ func TestWriteEnvFileUpdateDos(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueUpdate,
|
Vars: valueUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,9 +272,9 @@ func TestWriteEnvFileDos2Unix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueNoop,
|
Vars: valueNoop,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,9 +320,9 @@ func TestWriteEnvFileEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ef := EnvFile{
|
ef := EnvFile{
|
||||||
File: &File{
|
File: &File{config.File{
|
||||||
Path: name,
|
Path: name,
|
||||||
},
|
}},
|
||||||
Vars: valueEmpty,
|
Vars: valueEmpty,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,9 +362,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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -391,9 +393,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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,9 +415,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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package initialize
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -6,15 +6,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 config.EtcHosts
|
||||||
|
}
|
||||||
|
|
||||||
func (eh EtcHosts) generateEtcHosts() (out string, err error) {
|
func (eh EtcHosts) generateEtcHosts() (out string, err error) {
|
||||||
if eh != "localhost" {
|
if eh.Config != "localhost" {
|
||||||
return "", errors.New("Invalid option to manage_etc_hosts")
|
return "", errors.New("Invalid option to manage_etc_hosts")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,8 +30,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.Config == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,9 +40,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
|
||||||
}
|
}
|
46
system/etc_hosts_test.go
Normal file
46
system/etc_hosts_test.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
system/etcd.go
Normal file
25
system/etcd.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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, error) {
|
||||||
|
content := dropinContents(ee.Etcd)
|
||||||
|
if content == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return []Unit{{config.Unit{
|
||||||
|
Name: "etcd.service",
|
||||||
|
Runtime: true,
|
||||||
|
DropIn: true,
|
||||||
|
Content: content,
|
||||||
|
}}}, nil
|
||||||
|
}
|
60
system/etcd_test.go
Normal file
60
system/etcd_test.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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{},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config.Etcd{
|
||||||
|
Discovery: "http://disco.example.com/foobar",
|
||||||
|
PeerBindAddr: "127.0.0.1:7002",
|
||||||
|
},
|
||||||
|
[]Unit{{config.Unit{
|
||||||
|
Name: "etcd.service",
|
||||||
|
Runtime: true,
|
||||||
|
DropIn: true,
|
||||||
|
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,
|
||||||
|
DropIn: true,
|
||||||
|
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, err := Etcd{tt.config}.Units()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("bad error (%q): want %q, got %q", tt.config, nil, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(tt.units, units) {
|
||||||
|
t.Errorf("bad units (%q): want %#v, got %#v", tt.config, tt.units, units)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,14 +8,14 @@ import (
|
|||||||
"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) {
|
||||||
|
@ -5,6 +5,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWriteFileUnencodedContent(t *testing.T) {
|
func TestWriteFileUnencodedContent(t *testing.T) {
|
||||||
@ -17,11 +19,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 {
|
||||||
@ -56,11 +58,11 @@ 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")
|
||||||
@ -77,10 +79,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 {
|
||||||
@ -106,11 +108,11 @@ func TestWriteFileEncodedContent(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: "",
|
Content: "",
|
||||||
Encoding: "base64",
|
Encoding: "base64",
|
||||||
}
|
}}
|
||||||
|
|
||||||
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")
|
||||||
|
26
system/fleet.go
Normal file
26
system/fleet.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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, error) {
|
||||||
|
content := dropinContents(fe.Fleet)
|
||||||
|
if content == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return []Unit{{config.Unit{
|
||||||
|
Name: "fleet.service",
|
||||||
|
Runtime: true,
|
||||||
|
DropIn: true,
|
||||||
|
Content: content,
|
||||||
|
}}}, nil
|
||||||
|
}
|
41
system/fleet_test.go
Normal file
41
system/fleet_test.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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{},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config.Fleet{
|
||||||
|
PublicIP: "12.34.56.78",
|
||||||
|
},
|
||||||
|
[]Unit{{config.Unit{
|
||||||
|
Name: "fleet.service",
|
||||||
|
Content: `[Service]
|
||||||
|
Environment="FLEET_PUBLIC_IP=12.34.56.78"
|
||||||
|
`,
|
||||||
|
Runtime: true,
|
||||||
|
DropIn: true,
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
units, err := Fleet{tt.config}.Units()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("bad error (%q): want %q, got %q", tt.config, nil, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(units, tt.units) {
|
||||||
|
t.Errorf("bad units (%q): want %q, got %q", tt.config, tt.units, units)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
"github.com/coreos/coreos-cloudinit/network"
|
"github.com/coreos/coreos-cloudinit/network"
|
||||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/dotcloud/docker/pkg/netlink"
|
"github.com/coreos/coreos-cloudinit/third_party/github.com/dotcloud/docker/pkg/netlink"
|
||||||
)
|
)
|
||||||
@ -108,11 +109,11 @@ func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeConfig(filename string, config string) error {
|
func writeConfig(filename string, content string) error {
|
||||||
if config == "" {
|
if content == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Printf("Writing networkd unit %q\n", filename)
|
log.Printf("Writing networkd unit %q\n", filename)
|
||||||
_, err := WriteFile(&File{Content: config, Path: filename}, runtimeNetworkPath)
|
_, err := WriteFile(&File{config.File{Content: content, Path: filename}}, runtimeNetworkPath)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
32
system/oem.go
Normal file
32
system/oem.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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
|
||||||
|
}
|
47
system/oem_test.go
Normal file
47
system/oem_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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 %q, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
|
"github.com/coreos/coreos-cloudinit/third_party/github.com/coreos/go-systemd/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,11 +36,11 @@ func (s *systemd) PlaceUnit(u *Unit, dst string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
file := File{
|
file := File{config.File{
|
||||||
Path: filepath.Base(dst),
|
Path: filepath.Base(dst),
|
||||||
Content: u.Content,
|
Content: u.Content,
|
||||||
RawFilePermissions: "0644",
|
RawFilePermissions: "0644",
|
||||||
}
|
}}
|
||||||
|
|
||||||
_, err := WriteFile(&file, dir)
|
_, err := WriteFile(&file, dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -5,10 +5,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPlaceNetworkUnit(t *testing.T) {
|
func TestPlaceNetworkUnit(t *testing.T) {
|
||||||
u := Unit{
|
u := Unit{config.Unit{
|
||||||
Name: "50-eth0.network",
|
Name: "50-eth0.network",
|
||||||
Runtime: true,
|
Runtime: true,
|
||||||
Content: `[Match]
|
Content: `[Match]
|
||||||
@ -17,7 +19,7 @@ Name=eth47
|
|||||||
[Network]
|
[Network]
|
||||||
Address=10.209.171.177/19
|
Address=10.209.171.177/19
|
||||||
`,
|
`,
|
||||||
}
|
}}
|
||||||
|
|
||||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -66,10 +68,10 @@ func TestUnitDestination(t *testing.T) {
|
|||||||
dir := "/some/dir"
|
dir := "/some/dir"
|
||||||
name := "foobar.service"
|
name := "foobar.service"
|
||||||
|
|
||||||
u := Unit{
|
u := Unit{config.Unit{
|
||||||
Name: name,
|
Name: name,
|
||||||
DropIn: false,
|
DropIn: false,
|
||||||
}
|
}}
|
||||||
|
|
||||||
dst := u.Destination(dir)
|
dst := u.Destination(dir)
|
||||||
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
|
expectDst := path.Join(dir, "etc", "systemd", "system", "foobar.service")
|
||||||
@ -87,14 +89,14 @@ func TestUnitDestination(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPlaceMountUnit(t *testing.T) {
|
func TestPlaceMountUnit(t *testing.T) {
|
||||||
u := Unit{
|
u := Unit{config.Unit{
|
||||||
Name: "media-state.mount",
|
Name: "media-state.mount",
|
||||||
Runtime: false,
|
Runtime: false,
|
||||||
Content: `[Mount]
|
Content: `[Mount]
|
||||||
What=/dev/sdb1
|
What=/dev/sdb1
|
||||||
Where=/media/state
|
Where=/media/state
|
||||||
`,
|
`,
|
||||||
}
|
}}
|
||||||
|
|
||||||
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
dir, err := ioutil.TempDir(os.TempDir(), "coreos-cloudinit-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -162,7 +164,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)
|
||||||
}
|
}
|
||||||
@ -176,7 +178,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)
|
||||||
@ -202,12 +204,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)
|
||||||
@ -227,7 +229,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)
|
||||||
|
@ -3,8 +3,8 @@ package system
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"github.com/coreos/coreos-cloudinit/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Name for drop-in service configuration files created by cloudconfig
|
// Name for drop-in service configuration files created by cloudconfig
|
||||||
@ -19,33 +19,10 @@ type UnitManager interface {
|
|||||||
UnmaskUnit(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 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 {
|
|
||||||
ext := filepath.Ext(u.Name)
|
|
||||||
return strings.TrimLeft(ext, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unit) Group() (group string) {
|
|
||||||
t := u.Type()
|
|
||||||
if t == "network" || t == "netdev" || t == "link" {
|
|
||||||
group = "network"
|
|
||||||
} else {
|
|
||||||
group = "system"
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Script []byte
|
type Script []byte
|
||||||
|
137
system/update.go
Normal file
137
system/update.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
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 {
|
||||||
|
Config config.Update
|
||||||
|
ReadConfig func() (io.Reader, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Config) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err := config.AssertValid(uc.Config); 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.Config)
|
||||||
|
ucv := reflect.ValueOf(uc.Config)
|
||||||
|
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, error) {
|
||||||
|
var units []Unit
|
||||||
|
if uc.Config.RebootStrategy != "" {
|
||||||
|
ls := &Unit{config.Unit{
|
||||||
|
Name: locksmithUnit,
|
||||||
|
Command: "restart",
|
||||||
|
Mask: false,
|
||||||
|
Runtime: true,
|
||||||
|
}}
|
||||||
|
|
||||||
|
if uc.Config.RebootStrategy == "off" {
|
||||||
|
ls.Command = "stop"
|
||||||
|
ls.Mask = true
|
||||||
|
}
|
||||||
|
units = append(units, *ls)
|
||||||
|
}
|
||||||
|
|
||||||
|
if uc.Config.Group != "" || uc.Config.Server != "" {
|
||||||
|
ue := Unit{config.Unit{
|
||||||
|
Name: updateEngineUnit,
|
||||||
|
Command: "restart",
|
||||||
|
}}
|
||||||
|
units = append(units, ue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return units, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedKeys(m map[string]string) (keys []string) {
|
||||||
|
for key := range m {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
return
|
||||||
|
}
|
151
system/update_test.go
Normal file
151
system/update_test.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"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: "off"},
|
||||||
|
units: []Unit{{config.Unit{
|
||||||
|
Name: "locksmithd.service",
|
||||||
|
Command: "stop",
|
||||||
|
Runtime: true,
|
||||||
|
Mask: true,
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
units, err := Update{tt.config, testReadConfig("")}.Units()
|
||||||
|
if !reflect.DeepEqual(tt.err, err) {
|
||||||
|
t.Errorf("bad error (%q): want %q, got %q", tt.config, tt.err, err)
|
||||||
|
}
|
||||||
|
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: errors.New("invalid value \"wizzlewazzle\" for option \"RebootStrategy\" (valid options: \"best-effort,etcd-lock,reboot,off\")"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: "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{tt.config, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,30 +6,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 != "" {
|
||||||
|
29
test
29
test
@ -13,19 +13,22 @@ COVER=${COVER:-"-cover"}
|
|||||||
|
|
||||||
source ./build
|
source ./build
|
||||||
|
|
||||||
declare -a TESTPKGS=(initialize
|
declare -a TESTPKGS=(
|
||||||
system
|
config
|
||||||
datasource
|
datasource
|
||||||
datasource/configdrive
|
datasource/configdrive
|
||||||
datasource/file
|
datasource/file
|
||||||
datasource/metadata
|
datasource/metadata
|
||||||
datasource/metadata/cloudsigma
|
datasource/metadata/cloudsigma
|
||||||
datasource/metadata/digitalocean
|
datasource/metadata/digitalocean
|
||||||
datasource/metadata/ec2
|
datasource/metadata/ec2
|
||||||
datasource/proc_cmdline
|
datasource/proc_cmdline
|
||||||
datasource/url
|
datasource/url
|
||||||
pkg
|
initialize
|
||||||
network)
|
network
|
||||||
|
pkg
|
||||||
|
system
|
||||||
|
)
|
||||||
|
|
||||||
if [ -z "$PKG" ]; then
|
if [ -z "$PKG" ]; then
|
||||||
GOFMTPATH="${TESTPKGS[*]} coreos-cloudinit.go"
|
GOFMTPATH="${TESTPKGS[*]} coreos-cloudinit.go"
|
||||||
|
Loading…
Reference in New Issue
Block a user