diff --git a/api/groups.go b/api/groups.go new file mode 100644 index 00000000..bd394988 --- /dev/null +++ b/api/groups.go @@ -0,0 +1,38 @@ +package api + +import ( + "errors" + + "gopkg.in/yaml.v2" +) + +// GroupConfig parser errors +var ( + ErrInvalidVersion = errors.New("api: mismatched API version") +) + +// Group associates matcher conditions with a Specification identifier. +type Group struct { + // Human readable name (optional) + Name string `yaml:"name"` + // Spec identifier + Specification string `yaml:"spec"` + // matcher conditions + Matcher RequirementSet `yaml:"require"` +} + +// GroupConfig define an group import structure. +type GroupConfig struct { + APIVersion string `yaml:"api_version"` + Groups []Group `yaml:"groups"` +} + +// ParseGroupConfig parses a YAML group config and returns a GroupConfig. +func ParseGroupConfig(data []byte) (*GroupConfig, error) { + config := new(GroupConfig) + err := yaml.Unmarshal(data, config) + if err == nil && config.APIVersion != APIVersion { + return nil, ErrInvalidVersion + } + return config, err +} diff --git a/api/groups_test.go b/api/groups_test.go new file mode 100644 index 00000000..320c3d2e --- /dev/null +++ b/api/groups_test.go @@ -0,0 +1,47 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseGroupConfig(t *testing.T) { + validData := ` +api_version: v1alpha1 +groups: + - name: node1 + spec: worker-central + require: + role: worker + region: us-central1-a +` + validConfig := &GroupConfig{ + APIVersion: "v1alpha1", + Groups: []Group{ + Group{ + Name: "node1", + Specification: "worker-central", + Matcher: RequirementSet(map[string]string{ + "role": "worker", + "region": "us-central1-a", + }), + }, + }, + } + wrongVersion := `api_version:` + + cases := []struct { + data string + expectedConfig *GroupConfig + expectedErr error + }{ + {validData, validConfig, nil}, + {wrongVersion, nil, ErrInvalidVersion}, + } + for _, c := range cases { + config, err := ParseGroupConfig([]byte(c.data)) + assert.Equal(t, c.expectedConfig, config) + assert.Equal(t, c.expectedErr, err) + } +} diff --git a/api/matchers.go b/api/matchers.go new file mode 100644 index 00000000..6dc59b8f --- /dev/null +++ b/api/matchers.go @@ -0,0 +1,30 @@ +package api + +// RequirementSet is a map of key:value equality requirements which +// match against any Labels which are supersets. +type RequirementSet map[string]string + +// Matches returns true if the given labels satisfy all the requirements, +// false otherwise. +func (r RequirementSet) Matches(labels Labels) bool { + for k, v := range r { + if labels.Get(k) != v { + return false + } + } + return true +} + +// Labels present key to value mappings, independent of their storage. +type Labels interface { + // Get returns the value for the given label. + Get(label string) string +} + +// LabelSet is a map of key:value labels. +type LabelSet map[string]string + +// Get returns the value for the given label. +func (ls LabelSet) Get(label string) string { + return ls[label] +} diff --git a/api/matchers_test.go b/api/matchers_test.go new file mode 100644 index 00000000..0472dc95 --- /dev/null +++ b/api/matchers_test.go @@ -0,0 +1,53 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRequirementMatches(t *testing.T) { + // requirements + reqs := map[string]string{ + "region": "Central US", + "zone": "us-central1-a", + "lot": "42", + } + attrs := map[string]string{ + "uuid": "16e7d8a7-bfa9-428b-9117-363341bb330b", + } + // labels + labels := map[string]string{ + "region": "Central US", + "zone": "us-central1-a", + "lot": "42", + } + query := map[string]string{ + "uuid": "16e7d8a7-bfa9-428b-9117-363341bb330b", + } + lacking := map[string]string{ + "region": "Central US", + } + + cases := []struct { + reqs map[string]string + labels map[string]string + expected bool + }{ + {reqs, labels, true}, + {attrs, query, true}, + {reqs, lacking, false}, + // zero requirements match any label set + {map[string]string{}, labels, true}, + } + for _, c := range cases { + r := RequirementSet(c.reqs) + l := LabelSet(c.labels) + assert.Equal(t, c.expected, r.Matches(l)) + } +} + +func TestLabelSetGet(t *testing.T) { + labels := LabelSet(map[string]string{"a": "b"}) + assert.Equal(t, "b", labels.Get("a")) +} diff --git a/api/server.go b/api/server.go index d5ea4cc2..5eb36224 100644 --- a/api/server.go +++ b/api/server.go @@ -6,6 +6,11 @@ import ( "github.com/coreos/pkg/capnslog" ) +const ( + // APIVersion of the api server and its config types. + APIVersion = "v1alpha1" +) + var log = capnslog.NewPackageLogger("github.com/coreos/coreos-baremetal", "api") // Config configures the api Server. diff --git a/cmd/bootcfg/main.go b/cmd/bootcfg/main.go index 90f30e38..cba0ed3f 100644 --- a/cmd/bootcfg/main.go +++ b/cmd/bootcfg/main.go @@ -2,6 +2,7 @@ package main import ( "flag" + "io/ioutil" "net/http" "net/url" "os" @@ -17,6 +18,7 @@ var log = capnslog.NewPackageLogger("github.com/coreos/coreos-baremetal/cmd/boot func main() { flags := flag.NewFlagSet("bootcfg", flag.ExitOnError) address := flags.String("address", "127.0.0.1:8080", "HTTP listen address") + configPath := flags.String("config", "", "Path to config file") dataPath := flags.String("data-path", "./data", "Path to data directory") imagesPath := flags.String("images-path", "./images", "Path to static assets") // available log levels https://godoc.org/github.com/coreos/pkg/capnslog#LogLevel @@ -44,13 +46,28 @@ func main() { // logging setup lvl, err := capnslog.ParseLevel(strings.ToUpper(*logLevel)) if err != nil { - log.Fatalf("Invalid log-level: %s", err.Error()) + log.Fatalf("Invalid log-level: %v", err.Error()) } capnslog.SetGlobalLogLevel(lvl) capnslog.SetFormatter(capnslog.NewPrettyFormatter(os.Stdout, false)) + // storage + store := api.NewFileStore(http.Dir(*dataPath)) + + // bootstrap a group config + if *configPath != "" { + data, err := ioutil.ReadFile(*configPath) + if err != nil { + log.Fatalf("error reading config file: %v", err) + } + _, err = api.ParseGroupConfig(data) + if err != nil { + log.Fatalf("error parsing group config: %v", err) + } + } + config := &api.Config{ - Store: api.NewFileStore(http.Dir(*dataPath)), + Store: store, ImagePath: *imagesPath, }