api: Add Groups and GroupConfig with Requirements and Labels

* Add Group definitions to associate attribute matchers to particular
Spec specifications to supercede use of machine.json files
This commit is contained in:
Dalton Hubble
2016-01-07 14:17:28 -08:00
parent 64420c12b6
commit dae760e5bd
6 changed files with 192 additions and 2 deletions

38
api/groups.go Normal file
View File

@@ -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
}

47
api/groups_test.go Normal file
View File

@@ -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)
}
}

30
api/matchers.go Normal file
View File

@@ -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]
}

53
api/matchers_test.go Normal file
View File

@@ -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"))
}

View File

@@ -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.

View File

@@ -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,
}