cloudconfig: refactor config
- Move CloudConfig into config package - Add YAML tags to CloudConfig
This commit is contained in:
		
							
								
								
									
										132
									
								
								config/config.go
									
									
									
									
									
								
							
							
						
						
									
										132
									
								
								config/config.go
									
									
									
									
									
								
							| @@ -2,10 +2,58 @@ 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 { | ||||
| @@ -64,3 +112,87 @@ func isValid(v reflect.Value, valid string) bool { | ||||
| 	} | ||||
| 	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) | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,9 @@ package config | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| @@ -61,3 +63,411 @@ func TestAssertValid(t *testing.T) { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import ( | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
| 	"github.com/coreos/coreos-cloudinit/datasource" | ||||
| 	"github.com/coreos/coreos-cloudinit/datasource/configdrive" | ||||
| 	"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) | ||||
| 	userdata := env.Apply(string(userdataBytes)) | ||||
|  | ||||
| 	var ccm, ccu *initialize.CloudConfig | ||||
| 	var ccm, ccu *config.CloudConfig | ||||
| 	var script *system.Script | ||||
| 	if ccm, err = initialize.ParseMetaData(string(metadataBytes)); err != nil { | ||||
| 		fmt.Printf("Failed to parse meta-data: %v\n", err) | ||||
| @@ -178,14 +179,14 @@ func main() { | ||||
| 		failure = true | ||||
| 	} else { | ||||
| 		switch t := ud.(type) { | ||||
| 		case *initialize.CloudConfig: | ||||
| 		case *config.CloudConfig: | ||||
| 			ccu = t | ||||
| 		case system.Script: | ||||
| 			script = &t | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var cc *initialize.CloudConfig | ||||
| 	var cc *config.CloudConfig | ||||
| 	if ccm != nil && ccu != nil { | ||||
| 		fmt.Println("Merging cloud-config from meta-data and user-data") | ||||
| 		merged := mergeCloudConfig(*ccm, *ccu) | ||||
| @@ -224,7 +225,7 @@ func main() { | ||||
| // 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 | ||||
| // 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 udcc.Hostname != "" { | ||||
| 			fmt.Printf("Warning: user-data hostname (%s) overrides metadata hostname (%s)\n", udcc.Hostname, mdcc.Hostname) | ||||
|   | ||||
| @@ -4,38 +4,37 @@ import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/initialize" | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
| ) | ||||
|  | ||||
| func TestMergeCloudConfig(t *testing.T) { | ||||
| 	simplecc := initialize.CloudConfig{ | ||||
| 	simplecc := config.CloudConfig{ | ||||
| 		SSHAuthorizedKeys: []string{"abc", "def"}, | ||||
| 		Hostname:          "foobar", | ||||
| 		NetworkConfigPath: "/path/somewhere", | ||||
| 		NetworkConfig:     `{}`, | ||||
| 	} | ||||
| 	for i, tt := range []struct { | ||||
| 		udcc initialize.CloudConfig | ||||
| 		mdcc initialize.CloudConfig | ||||
| 		want initialize.CloudConfig | ||||
| 		udcc config.CloudConfig | ||||
| 		mdcc config.CloudConfig | ||||
| 		want config.CloudConfig | ||||
| 	}{ | ||||
| 		{ | ||||
| 			// If mdcc is empty, udcc should be returned unchanged | ||||
| 			simplecc, | ||||
| 			initialize.CloudConfig{}, | ||||
| 			config.CloudConfig{}, | ||||
| 			simplecc, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// If udcc is empty, mdcc should be returned unchanged(overridden) | ||||
| 			initialize.CloudConfig{}, | ||||
| 			config.CloudConfig{}, | ||||
| 			simplecc, | ||||
| 			simplecc, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// user-data should override completely in the case of conflicts | ||||
| 			simplecc, | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				Hostname:          "meta-hostname", | ||||
| 				NetworkConfigPath: "/path/meta", | ||||
| 				NetworkConfig:     `{"hostname":"test"}`, | ||||
| @@ -44,17 +43,17 @@ func TestMergeCloudConfig(t *testing.T) { | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Mixed merge should succeed | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				SSHAuthorizedKeys: []string{"abc", "def"}, | ||||
| 				Hostname:          "user-hostname", | ||||
| 				NetworkConfigPath: "/path/somewhere", | ||||
| 				NetworkConfig:     `{"hostname":"test"}`, | ||||
| 			}, | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				SSHAuthorizedKeys: []string{"woof", "qux"}, | ||||
| 				Hostname:          "meta-hostname", | ||||
| 			}, | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				SSHAuthorizedKeys: []string{"abc", "def", "woof", "qux"}, | ||||
| 				Hostname:          "user-hostname", | ||||
| 				NetworkConfigPath: "/path/somewhere", | ||||
| @@ -63,15 +62,15 @@ func TestMergeCloudConfig(t *testing.T) { | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Completely non-conflicting merge should be fine | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				Hostname: "supercool", | ||||
| 			}, | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"}, | ||||
| 				NetworkConfigPath: "/dev/fun", | ||||
| 				NetworkConfig:     `{"hostname":"test"}`, | ||||
| 			}, | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				Hostname:          "supercool", | ||||
| 				SSHAuthorizedKeys: []string{"zaphod", "beeblebrox"}, | ||||
| 				NetworkConfigPath: "/dev/fun", | ||||
| @@ -80,16 +79,16 @@ func TestMergeCloudConfig(t *testing.T) { | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Non-mergeable settings in user-data should not be affected | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				Hostname:       "mememe", | ||||
| 				ManageEtcHosts: config.EtcHosts("lolz"), | ||||
| 			}, | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				Hostname:          "youyouyou", | ||||
| 				NetworkConfigPath: "meta-meta-yo", | ||||
| 				NetworkConfig:     `{"hostname":"test"}`, | ||||
| 			}, | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				Hostname:          "mememe", | ||||
| 				ManageEtcHosts:    config.EtcHosts("lolz"), | ||||
| 				NetworkConfigPath: "meta-meta-yo", | ||||
| @@ -98,15 +97,15 @@ func TestMergeCloudConfig(t *testing.T) { | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Non-mergeable (unexpected) settings in meta-data are ignored | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				Hostname: "mememe", | ||||
| 			}, | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				ManageEtcHosts:    config.EtcHosts("lolz"), | ||||
| 				NetworkConfigPath: "meta-meta-yo", | ||||
| 				NetworkConfig:     `{"hostname":"test"}`, | ||||
| 			}, | ||||
| 			initialize.CloudConfig{ | ||||
| 			config.CloudConfig{ | ||||
| 				Hostname:          "mememe", | ||||
| 				NetworkConfigPath: "meta-meta-yo", | ||||
| 				NetworkConfig:     `{"hostname":"test"}`, | ||||
|   | ||||
| @@ -6,8 +6,6 @@ import ( | ||||
| 	"log" | ||||
| 	"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/system" | ||||
| @@ -27,137 +25,10 @@ type CloudConfigUnit interface { | ||||
| 	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   config.Etcd | ||||
| 		Fleet  config.Fleet | ||||
| 		OEM    config.OEM | ||||
| 		Update config.Update | ||||
| 		Units  []config.Unit | ||||
| 	} | ||||
| 	WriteFiles        []config.File `yaml:"write_files"` | ||||
| 	Hostname          string | ||||
| 	Users             []config.User | ||||
| 	ManageEtcHosts    config.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(&config.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 | ||||
| // configuring the hostname, adding new users, writing various configuration | ||||
| // 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 err := system.SetHostname(cfg.Hostname); err != nil { | ||||
| 			return err | ||||
|   | ||||
| @@ -1,369 +1,12 @@ | ||||
| package initialize | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
| 	"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 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 := system.File{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.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") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type TestUnitManager struct { | ||||
| 	placed   []string | ||||
| 	enabled  []string | ||||
|   | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
| @@ -3,11 +3,13 @@ package initialize | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"sort" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
| ) | ||||
|  | ||||
| // ParseMetaData parses a JSON blob in the OpenStack metadata service format, | ||||
| // 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 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| @@ -22,7 +24,7 @@ func ParseMetaData(contents string) (*CloudConfig, error) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var cfg CloudConfig | ||||
| 	var cfg config.CloudConfig | ||||
| 	if len(metadata.SSHAuthorizedKeyMap) > 0 { | ||||
| 		cfg.SSHAuthorizedKeys = make([]string, 0, len(metadata.SSHAuthorizedKeyMap)) | ||||
| 		for _, name := range sortedKeys(metadata.SSHAuthorizedKeyMap) { | ||||
|   | ||||
| @@ -1,21 +1,25 @@ | ||||
| package initialize | ||||
|  | ||||
| import "reflect" | ||||
| import "testing" | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
| ) | ||||
|  | ||||
| func TestParseMetadata(t *testing.T) { | ||||
| 	for i, tt := range []struct { | ||||
| 		in   string | ||||
| 		want *CloudConfig | ||||
| 		want *config.CloudConfig | ||||
| 		err  bool | ||||
| 	}{ | ||||
| 		{"", nil, false}, | ||||
| 		{`garbage, invalid json`, nil, true}, | ||||
| 		{`{"foo": "bar"}`, &CloudConfig{}, false}, | ||||
| 		{`{"network_config": {"content_path": "asdf"}}`, &CloudConfig{NetworkConfigPath: "asdf"}, false}, | ||||
| 		{`{"hostname": "turkleton"}`, &CloudConfig{Hostname: "turkleton"}, false}, | ||||
| 		{`{"public_keys": {"jack": "jill", "bob": "alice"}}`, &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}, | ||||
| 		{`{"foo": "bar"}`, &config.CloudConfig{}, false}, | ||||
| 		{`{"network_config": {"content_path": "asdf"}}`, &config.CloudConfig{NetworkConfigPath: "asdf"}, false}, | ||||
| 		{`{"hostname": "turkleton"}`, &config.CloudConfig{Hostname: "turkleton"}, 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"}}`, &config.CloudConfig{SSHAuthorizedKeys: []string{"re", "fa"}, Hostname: "my_host", NetworkConfigPath: "/root"}, false}, | ||||
| 	} { | ||||
| 		got, err := ParseMetaData(tt.in) | ||||
| 		if tt.err != (err != nil) { | ||||
|   | ||||
| @@ -39,31 +39,4 @@ func TestCloudConfigUsersUrlMarshal(t *testing.T) { | ||||
| 	if keys[2] != expected { | ||||
| 		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) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import ( | ||||
| 	"log" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
| 	"github.com/coreos/coreos-cloudinit/system" | ||||
| ) | ||||
|  | ||||
| @@ -24,7 +25,7 @@ func ParseUserData(contents string) (interface{}, error) { | ||||
| 		return system.Script(contents), nil | ||||
| 	} else if header == "#cloud-config" { | ||||
| 		log.Printf("Parsing user-data as cloud-config") | ||||
| 		return NewCloudConfig(contents) | ||||
| 		return config.NewCloudConfig(contents) | ||||
| 	} else { | ||||
| 		return nil, fmt.Errorf("Unrecognized user-data header: %s", header) | ||||
| 	} | ||||
|   | ||||
| @@ -2,6 +2,8 @@ package initialize | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/coreos/coreos-cloudinit/config" | ||||
| ) | ||||
|  | ||||
| func TestParseHeaderCRLF(t *testing.T) { | ||||
| @@ -37,7 +39,7 @@ func TestParseConfigCRLF(t *testing.T) { | ||||
| 		t.Fatalf("Failed parsing config: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	cfg := ud.(*CloudConfig) | ||||
| 	cfg := ud.(*config.CloudConfig) | ||||
|  | ||||
| 	if cfg.Hostname != "foo" { | ||||
| 		t.Error("Failed parsing hostname from config") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user