mirror of
https://github.com/outbackdingo/matchbox.git
synced 2026-01-27 10:19:35 +00:00
bootcfg/api: Switch from api types to storagepb types
* Remove api.Store and use storagepb.Store instead * Remove api.Spec and use storagepb.Profile instead * Switch from api.Group to storagepb.Group * Move api.Group to config for YAML config decoding only
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## Latest
|
||||
|
||||
* Remove HTTP `/spec/id` JSON endpoint
|
||||
* Add detached OpenPGP signature endpoints (`.sig`)
|
||||
|
||||
## v0.2.0 (2016-02-09)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package api
|
||||
|
||||
// BootConfig defines a kernel image, kernel options, and initrds to boot.
|
||||
type BootConfig struct {
|
||||
// the URL of the kernel image
|
||||
Kernel string `json:"kernel"`
|
||||
// the init RAM filesystem URLs
|
||||
Initrd []string `json:"initrd"`
|
||||
// command line kernel options
|
||||
Cmdline map[string]interface{} `json:"cmdline"`
|
||||
}
|
||||
@@ -2,10 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage"
|
||||
cloudinit "github.com/coreos/coreos-cloudinit/config"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@@ -17,28 +19,31 @@ type CloudConfig struct {
|
||||
|
||||
// cloudHandler returns a handler that responds with the cloud config for the
|
||||
// requester.
|
||||
func cloudHandler(store Store) ContextHandler {
|
||||
func cloudHandler(store storage.Store) ContextHandler {
|
||||
fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
group, err := groupFromContext(ctx)
|
||||
if err != nil || group.Spec == "" {
|
||||
if err != nil || group.Profile == "" {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
spec, err := store.Spec(group.Spec)
|
||||
if err != nil || spec.CloudConfig == "" {
|
||||
profile, err := store.ProfileGet(group.Profile)
|
||||
if err != nil || profile.CloudId == "" {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
contents, err := store.CloudConfig(spec.CloudConfig)
|
||||
contents, err := store.CloudGet(profile.CloudId)
|
||||
if err != nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// collect data for rendering
|
||||
data := make(map[string]interface{})
|
||||
for k := range group.Metadata {
|
||||
data[k] = group.Metadata[k]
|
||||
var data map[string]interface{}
|
||||
err = json.Unmarshal(group.Metadata, &data)
|
||||
if err != nil {
|
||||
log.Error("error unmarshalling metadata")
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// render the template of a cloud config with data
|
||||
|
||||
@@ -5,28 +5,29 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestCloudHandler(t *testing.T) {
|
||||
cloudContent := "#cloud-config"
|
||||
content := "#cloud-config"
|
||||
store := &fixedStore{
|
||||
Specs: map[string]*Spec{testGroup.Spec: testSpec},
|
||||
CloudConfigs: map[string]string{testSpec.CloudConfig: cloudContent},
|
||||
Profiles: map[string]*storagepb.Profile{testGroup.Profile: testProfile},
|
||||
CloudConfigs: map[string]string{testProfile.CloudId: content},
|
||||
}
|
||||
h := cloudHandler(store)
|
||||
ctx := withGroup(context.Background(), &testGroup)
|
||||
ctx := withGroup(context.Background(), testGroup)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(ctx, w, req)
|
||||
// assert that:
|
||||
// - the Spec's cloud config is served
|
||||
// - Cloud config is rendered with Group metadata
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, cloudContent, w.Body.String())
|
||||
assert.Equal(t, content, w.Body.String())
|
||||
}
|
||||
|
||||
func TestCloudHandler_MissingCtxSpec(t *testing.T) {
|
||||
func TestCloudHandler_MissingCtxProfile(t *testing.T) {
|
||||
h := cloudHandler(&emptyStore{})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
@@ -36,7 +37,7 @@ func TestCloudHandler_MissingCtxSpec(t *testing.T) {
|
||||
|
||||
func TestCloudHandler_MissingCloudConfig(t *testing.T) {
|
||||
h := cloudHandler(&emptyStore{})
|
||||
ctx := withSpec(context.Background(), testSpec)
|
||||
ctx := withProfile(context.Background(), testProfile)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(ctx, w, req)
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@@ -10,37 +11,37 @@ import (
|
||||
type key int
|
||||
|
||||
const (
|
||||
specKey key = iota
|
||||
profileKey key = iota
|
||||
groupKey
|
||||
)
|
||||
|
||||
var (
|
||||
errNoSpecFromContext = errors.New("api: Context missing a Spec")
|
||||
errNoGroupFromContext = errors.New("api: Context missing a Group")
|
||||
errNoProfileFromContext = errors.New("api: Context missing a Profile")
|
||||
errNoGroupFromContext = errors.New("api: Context missing a Group")
|
||||
)
|
||||
|
||||
// withSpec returns a copy of ctx that stores the given Spec.
|
||||
func withSpec(ctx context.Context, spec *Spec) context.Context {
|
||||
return context.WithValue(ctx, specKey, spec)
|
||||
// withProfile returns a copy of ctx that stores the given Profile.
|
||||
func withProfile(ctx context.Context, profile *storagepb.Profile) context.Context {
|
||||
return context.WithValue(ctx, profileKey, profile)
|
||||
}
|
||||
|
||||
// specFromContext returns the Spec from the ctx.
|
||||
func specFromContext(ctx context.Context) (*Spec, error) {
|
||||
spec, ok := ctx.Value(specKey).(*Spec)
|
||||
// profileFromContext returns the Profile from the ctx.
|
||||
func profileFromContext(ctx context.Context) (*storagepb.Profile, error) {
|
||||
profile, ok := ctx.Value(profileKey).(*storagepb.Profile)
|
||||
if !ok {
|
||||
return nil, errNoSpecFromContext
|
||||
return nil, errNoProfileFromContext
|
||||
}
|
||||
return spec, nil
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// withGroup returns a copy of ctx that stores the given Group.
|
||||
func withGroup(ctx context.Context, group *Group) context.Context {
|
||||
func withGroup(ctx context.Context, group *storagepb.Group) context.Context {
|
||||
return context.WithValue(ctx, groupKey, group)
|
||||
}
|
||||
|
||||
// groupFromContext returns the Group from the ctx.
|
||||
func groupFromContext(ctx context.Context) (*Group, error) {
|
||||
group, ok := ctx.Value(groupKey).(*Group)
|
||||
func groupFromContext(ctx context.Context) (*storagepb.Group, error) {
|
||||
group, ok := ctx.Value(groupKey).(*storagepb.Group)
|
||||
if !ok {
|
||||
return nil, errNoGroupFromContext
|
||||
}
|
||||
|
||||
@@ -3,35 +3,36 @@ package api
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestContextSpec(t *testing.T) {
|
||||
expectedSpec := &Spec{ID: "g1h2i3j4"}
|
||||
ctx := withSpec(context.Background(), expectedSpec)
|
||||
spec, err := specFromContext(ctx)
|
||||
func TestContextProfile(t *testing.T) {
|
||||
expectedProfile := &storagepb.Profile{Id: "g1h2i3j4"}
|
||||
ctx := withProfile(context.Background(), expectedProfile)
|
||||
profile, err := profileFromContext(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expectedSpec, spec)
|
||||
assert.Equal(t, expectedProfile, profile)
|
||||
}
|
||||
|
||||
func TestContextSpec_Error(t *testing.T) {
|
||||
spec, err := specFromContext(context.Background())
|
||||
assert.Nil(t, spec)
|
||||
func TestContextProfile_Error(t *testing.T) {
|
||||
profile, err := profileFromContext(context.Background())
|
||||
assert.Nil(t, profile)
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Equal(t, errNoSpecFromContext, err)
|
||||
assert.Equal(t, errNoProfileFromContext, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupSpec(t *testing.T) {
|
||||
expectedGroup := &Group{Name: "test group"}
|
||||
func TestContextGroup(t *testing.T) {
|
||||
expectedGroup := &storagepb.Group{Name: "test group"}
|
||||
ctx := withGroup(context.Background(), expectedGroup)
|
||||
group, err := groupFromContext(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expectedGroup, group)
|
||||
}
|
||||
|
||||
func TestGroupSpec_Error(t *testing.T) {
|
||||
func TestContextGroup_Error(t *testing.T) {
|
||||
group, err := groupFromContext(context.Background())
|
||||
assert.Nil(t, group)
|
||||
if assert.NotNil(t, err) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage"
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@@ -12,76 +14,19 @@ var (
|
||||
errNoMatchingGroup = errors.New("api: No matching Group")
|
||||
)
|
||||
|
||||
// Group associates matcher conditions with a Specification identifier. The
|
||||
// Matcher conditions may be satisfied by zero or more machines.
|
||||
type Group struct {
|
||||
// Human readable name (optional)
|
||||
Name string `yaml:"name"`
|
||||
// Spec identifier
|
||||
Spec string `yaml:"spec"`
|
||||
// Custom Metadata
|
||||
Metadata map[string]interface{} `yaml:"metadata"`
|
||||
// matcher conditions
|
||||
Matcher RequirementSet `yaml:"require"`
|
||||
}
|
||||
|
||||
// byMatcher defines a collection of Group structs which have a deterministic
|
||||
// order in increasing number of Matchers, then by sorted key/value pair strings.
|
||||
// For example, Matcher {a:b, c:d} is ordered before {a:d, c:d} and {a:b, d:e}.
|
||||
type byMatcher []Group
|
||||
|
||||
func (g byMatcher) Len() int {
|
||||
return len(g)
|
||||
}
|
||||
|
||||
func (g byMatcher) Swap(i, j int) {
|
||||
g[i], g[j] = g[j], g[i]
|
||||
}
|
||||
|
||||
func (g byMatcher) Less(i, j int) bool {
|
||||
if len(g[i].Matcher) == len(g[j].Matcher) {
|
||||
return g[i].Matcher.String() < g[j].Matcher.String()
|
||||
}
|
||||
return len(g[i].Matcher) < len(g[j].Matcher)
|
||||
}
|
||||
|
||||
type groupsResource struct {
|
||||
store Store
|
||||
store storage.Store
|
||||
}
|
||||
|
||||
func newGroupsResource(store Store) *groupsResource {
|
||||
res := &groupsResource{
|
||||
func newGroupsResource(store storage.Store) *groupsResource {
|
||||
return &groupsResource{
|
||||
store: store,
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// listGroups lists all Group resources.
|
||||
func (gr *groupsResource) listGroups() ([]Group, error) {
|
||||
return gr.store.ListGroups()
|
||||
}
|
||||
|
||||
// matchSpecHandler returns a ContextHandler that matches machine requests
|
||||
// to a Spec and adds the Spec to the ctx and calls the next handler. The
|
||||
// next handler should handle the case that no matching Spec is found.
|
||||
func (gr *groupsResource) matchSpecHandler(next ContextHandler) ContextHandler {
|
||||
fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
attrs := labelsFromRequest(req)
|
||||
// match machine request
|
||||
group, err := gr.findMatch(attrs)
|
||||
if err == nil {
|
||||
// lookup Spec by id
|
||||
spec, err := gr.store.Spec(group.Spec)
|
||||
if err == nil {
|
||||
// add the Spec to the ctx for next handler
|
||||
ctx = withSpec(ctx, spec)
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(ctx, w, req)
|
||||
}
|
||||
return ContextHandlerFunc(fn)
|
||||
}
|
||||
|
||||
// matchGroupHandler returns a ContextHandler that matches machine requests to
|
||||
// a Group, adds the Group to the ctx, and calls the next handler. The next
|
||||
// handler should handle the case that no matching Group is found.
|
||||
func (gr *groupsResource) matchGroupHandler(next ContextHandler) ContextHandler {
|
||||
fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
attrs := labelsFromRequest(req)
|
||||
@@ -96,18 +41,39 @@ func (gr *groupsResource) matchGroupHandler(next ContextHandler) ContextHandler
|
||||
return ContextHandlerFunc(fn)
|
||||
}
|
||||
|
||||
// matchProfileHandler returns a ContextHandler that matches machine requests
|
||||
// to a Profile, adds Profile to the ctx, and calls the next handler. The
|
||||
// next handler should handle the case that no matching Profile is found.
|
||||
func (gr *groupsResource) matchProfileHandler(next ContextHandler) ContextHandler {
|
||||
fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
attrs := labelsFromRequest(req)
|
||||
// match machine request
|
||||
group, err := gr.findMatch(attrs)
|
||||
if err == nil {
|
||||
// lookup Profile by id
|
||||
profile, err := gr.store.ProfileGet(group.Profile)
|
||||
if err == nil {
|
||||
// add the Profile to the ctx for next handler
|
||||
ctx = withProfile(ctx, profile)
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(ctx, w, req)
|
||||
}
|
||||
return ContextHandlerFunc(fn)
|
||||
}
|
||||
|
||||
// findMatch returns the first Group whose Matcher is satisfied by the given
|
||||
// labels. Groups are attempted in sorted order, preferring those with
|
||||
// more matcher conditions, alphabetically.
|
||||
func (gr *groupsResource) findMatch(labels map[string]string) (*Group, error) {
|
||||
groups, err := gr.store.ListGroups()
|
||||
func (gr *groupsResource) findMatch(labels map[string]string) (*storagepb.Group, error) {
|
||||
groups, err := gr.store.GroupList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Sort(sort.Reverse(byMatcher(groups)))
|
||||
sort.Sort(sort.Reverse(storagepb.ByReqs(groups)))
|
||||
for _, group := range groups {
|
||||
if group.Matcher.Matches(labels) {
|
||||
return &group, nil
|
||||
if group.Matches(labels) {
|
||||
return group, nil
|
||||
}
|
||||
}
|
||||
return nil, errNoMatchingGroup
|
||||
|
||||
@@ -4,111 +4,37 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage"
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var (
|
||||
validMACStr = "52:da:00:89:d8:10"
|
||||
nonNormalizedMACStr = "52:dA:00:89:d8:10"
|
||||
testGroup = Group{
|
||||
Name: "test group",
|
||||
Spec: "g1h2i3j4",
|
||||
Metadata: map[string]interface{}{
|
||||
"k8s_version": "v1.1.2",
|
||||
"pod_network": "10.2.0.0/16",
|
||||
"service_name": "etcd2",
|
||||
},
|
||||
Matcher: RequirementSet(map[string]string{"uuid": "a1b2c3d4"}),
|
||||
}
|
||||
testGroupWithMAC = Group{
|
||||
Name: "machine with a MAC",
|
||||
Spec: "g1h2i3j4",
|
||||
Matcher: RequirementSet(map[string]string{"mac": validMACStr}),
|
||||
}
|
||||
testGroupNoSpec = &Group{
|
||||
Name: "test group with missing spec",
|
||||
Spec: "",
|
||||
Matcher: RequirementSet(map[string]string{"uuid": "a1b2c3d4"}),
|
||||
}
|
||||
)
|
||||
|
||||
func TestByMatcherSort(t *testing.T) {
|
||||
oneCondition := Group{
|
||||
Name: "two matcher conditions",
|
||||
Matcher: RequirementSet(map[string]string{
|
||||
"region": "a",
|
||||
}),
|
||||
}
|
||||
twoConditions := Group{
|
||||
Name: "group with two matcher conditions",
|
||||
Matcher: RequirementSet(map[string]string{
|
||||
"region": "a",
|
||||
"zone": "z",
|
||||
}),
|
||||
}
|
||||
dualConditions := Group{
|
||||
Name: "another group with two matcher conditions",
|
||||
Matcher: RequirementSet(map[string]string{
|
||||
"region": "b",
|
||||
"zone": "z",
|
||||
}),
|
||||
}
|
||||
cases := []struct {
|
||||
input []Group
|
||||
expected []Group
|
||||
}{
|
||||
{[]Group{oneCondition, dualConditions, twoConditions}, []Group{oneCondition, twoConditions, dualConditions}},
|
||||
{[]Group{twoConditions, dualConditions, oneCondition}, []Group{oneCondition, twoConditions, dualConditions}},
|
||||
{[]Group{testGroup, testGroupWithMAC, oneCondition, twoConditions, dualConditions}, []Group{testGroupWithMAC, oneCondition, testGroup, twoConditions, dualConditions}},
|
||||
}
|
||||
// assert that
|
||||
// - groups are sorted in increasing Matcher length
|
||||
// - when Matcher lengths are equal, groups are sorted by key=value strings.
|
||||
// - group ordering is deterministic
|
||||
for _, c := range cases {
|
||||
sort.Sort(byMatcher(c.input))
|
||||
assert.Equal(t, c.expected, c.input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGroupsResource(t *testing.T) {
|
||||
store := &fixedStore{}
|
||||
gr := newGroupsResource(store)
|
||||
assert.Equal(t, store, gr.store)
|
||||
}
|
||||
|
||||
func TestGroupsResource_ListGroups(t *testing.T) {
|
||||
expectedGroups := []Group{Group{Name: "test group"}}
|
||||
func TestGroupsResource_MatchProfileHandler(t *testing.T) {
|
||||
store := &fixedStore{
|
||||
Groups: expectedGroups,
|
||||
}
|
||||
res := newGroupsResource(store)
|
||||
groups, err := res.listGroups()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expectedGroups, groups)
|
||||
}
|
||||
|
||||
func TestGroupsResource_MatchSpecHandler(t *testing.T) {
|
||||
store := &fixedStore{
|
||||
Groups: []Group{testGroup},
|
||||
Specs: map[string]*Spec{testGroup.Spec: testSpec},
|
||||
Groups: map[string]*storagepb.Group{testGroup.Id: testGroup},
|
||||
Profiles: map[string]*storagepb.Profile{testGroup.Profile: testProfile},
|
||||
}
|
||||
gr := newGroupsResource(store)
|
||||
next := func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
spec, err := specFromContext(ctx)
|
||||
profile, err := profileFromContext(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, testSpec, spec)
|
||||
assert.Equal(t, testProfile, profile)
|
||||
fmt.Fprintf(w, "next handler called")
|
||||
}
|
||||
// assert that:
|
||||
// - request arguments are used to match uuid=a1b2c3d4 -> testGroup
|
||||
// - the group's Spec is found by id and added to the context
|
||||
// - the group's Profile is found by id and added to the context
|
||||
// - next handler is called
|
||||
h := gr.matchSpecHandler(ContextHandlerFunc(next))
|
||||
h := gr.matchProfileHandler(ContextHandlerFunc(next))
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "?uuid=a1b2c3d4", nil)
|
||||
h.ServeHTTP(context.Background(), w, req)
|
||||
@@ -117,13 +43,13 @@ func TestGroupsResource_MatchSpecHandler(t *testing.T) {
|
||||
|
||||
func TestGroupsResource_MatchGroupHandler(t *testing.T) {
|
||||
store := &fixedStore{
|
||||
Groups: []Group{testGroup},
|
||||
Groups: map[string]*storagepb.Group{testGroup.Id: testGroup},
|
||||
}
|
||||
gr := newGroupsResource(store)
|
||||
next := func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
group, err := groupFromContext(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, &testGroup, group)
|
||||
assert.Equal(t, testGroup, group)
|
||||
fmt.Fprintf(w, "next handler called")
|
||||
}
|
||||
// assert that:
|
||||
@@ -138,23 +64,18 @@ func TestGroupsResource_MatchGroupHandler(t *testing.T) {
|
||||
|
||||
func TestGroupsResource_FindMatch(t *testing.T) {
|
||||
store := &fixedStore{
|
||||
Groups: []Group{testGroup},
|
||||
Specs: map[string]*Spec{testGroup.Spec: testSpec},
|
||||
Groups: map[string]*storagepb.Group{testGroup.Id: testGroup},
|
||||
}
|
||||
uuidLabel := map[string]string{
|
||||
"uuid": "a1b2c3d4",
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
store Store
|
||||
store storage.Store
|
||||
labels map[string]string
|
||||
expectedGroup *Group
|
||||
expectedGroup *storagepb.Group
|
||||
expectedErr error
|
||||
}{
|
||||
{store, uuidLabel, &testGroup, nil},
|
||||
{store, map[string]string{"uuid": "a1b2c3d4"}, testGroup, nil},
|
||||
{store, nil, nil, errNoMatchingGroup},
|
||||
// no groups in the store
|
||||
{&emptyStore{}, uuidLabel, nil, errNoMatchingGroup},
|
||||
{&emptyStore{}, map[string]string{"a": "b"}, nil, errNoMatchingGroup},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
||||
@@ -22,13 +22,13 @@ initrdefi {{ range $element := .Initrd }}"{{$element}}" {{end}}
|
||||
// requester.
|
||||
func grubHandler() ContextHandler {
|
||||
fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
spec, err := specFromContext(ctx)
|
||||
profile, err := profileFromContext(ctx)
|
||||
if err != nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = grubTemplate.Execute(&buf, spec.BootConfig)
|
||||
err = grubTemplate.Execute(&buf, profile.Boot)
|
||||
if err != nil {
|
||||
log.Errorf("error rendering template: %v", err)
|
||||
http.NotFound(w, req)
|
||||
|
||||
@@ -48,7 +48,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.handler.ServeHTTP(h.ctx, w, req)
|
||||
}
|
||||
|
||||
// labelsFromRequest returns Labels from request query parameters.
|
||||
// labelsFromRequest returns request query parameters.
|
||||
func labelsFromRequest(req *http.Request) map[string]string {
|
||||
values := req.URL.Query()
|
||||
labels := map[string]string{}
|
||||
|
||||
@@ -2,40 +2,45 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"gopkg.in/yaml.v2"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage"
|
||||
ignition "github.com/coreos/ignition/src/config"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ignitionHandler returns a handler that responds with the Ignition config
|
||||
// for the requester. The Ignition file referenced in the Spec is rendered
|
||||
// for the requester. The Ignition file referenced in the Profile is rendered
|
||||
// with metadata and parsed and validated as either YAML or JSON based on the
|
||||
// extension. The Ignition config is served as an HTTP JSON response.
|
||||
func ignitionHandler(store Store) ContextHandler {
|
||||
func ignitionHandler(store storage.Store) ContextHandler {
|
||||
fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
group, err := groupFromContext(ctx)
|
||||
if err != nil || group.Spec == "" {
|
||||
if err != nil || group.Profile == "" {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
spec, err := store.Spec(group.Spec)
|
||||
if err != nil || spec.IgnitionConfig == "" {
|
||||
profile, err := store.ProfileGet(group.Profile)
|
||||
if err != nil || profile.IgnitionId == "" {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
contents, err := store.IgnitionConfig(spec.IgnitionConfig)
|
||||
contents, err := store.IgnitionGet(profile.IgnitionId)
|
||||
if err != nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// collect data for rendering Ignition Config
|
||||
data := make(map[string]interface{})
|
||||
for k := range group.Metadata {
|
||||
data[k] = group.Metadata[k]
|
||||
var data map[string]interface{}
|
||||
err = json.Unmarshal(group.Metadata, &data)
|
||||
if err != nil {
|
||||
log.Errorf("error unmarshalling metadata: %v", err)
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
data["query"] = req.URL.RawQuery
|
||||
|
||||
@@ -49,7 +54,7 @@ func ignitionHandler(store Store) ContextHandler {
|
||||
|
||||
// Unmarshal YAML or JSON Ignition config
|
||||
var cfg ignition.Config
|
||||
if isYAML(spec.IgnitionConfig) {
|
||||
if isYAML(profile.IgnitionId) {
|
||||
if err := yaml.Unmarshal(buf.Bytes(), &cfg); err != nil {
|
||||
log.Errorf("error parsing YAML Ignition config: %v", err)
|
||||
http.NotFound(w, req)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@@ -14,11 +15,11 @@ var expectedIgnition = `{"ignitionVersion":1,"storage":{},"systemd":{"units":[{"
|
||||
func TestIgnitionHandler(t *testing.T) {
|
||||
content := `{"ignitionVersion": 1,"systemd":{"units":[{"name":"{{.service_name}}.service","enable":true}]}}`
|
||||
store := &fixedStore{
|
||||
Specs: map[string]*Spec{testGroup.Spec: testSpec},
|
||||
IgnitionConfigs: map[string]string{testSpec.IgnitionConfig: content},
|
||||
Profiles: map[string]*storagepb.Profile{testGroup.Profile: testProfile},
|
||||
IgnitionConfigs: map[string]string{testProfile.IgnitionId: content},
|
||||
}
|
||||
h := ignitionHandler(store)
|
||||
ctx := withGroup(context.Background(), &testGroup)
|
||||
ctx := withGroup(context.Background(), testGroup)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(ctx, w, req)
|
||||
@@ -40,11 +41,11 @@ systemd:
|
||||
enable: true
|
||||
`
|
||||
store := &fixedStore{
|
||||
Specs: map[string]*Spec{testGroup.Spec: testSpecWithIgnitionYAML},
|
||||
IgnitionConfigs: map[string]string{testSpecWithIgnitionYAML.IgnitionConfig: content},
|
||||
Profiles: map[string]*storagepb.Profile{testGroup.Profile: testProfileIgnitionYAML},
|
||||
IgnitionConfigs: map[string]string{testProfileIgnitionYAML.IgnitionId: content},
|
||||
}
|
||||
h := ignitionHandler(store)
|
||||
ctx := withGroup(context.Background(), &testGroup)
|
||||
ctx := withGroup(context.Background(), testGroup)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(ctx, w, req)
|
||||
@@ -57,7 +58,7 @@ systemd:
|
||||
assert.Equal(t, expectedIgnition, w.Body.String())
|
||||
}
|
||||
|
||||
func TestIgnitionHandler_MissingCtxSpec(t *testing.T) {
|
||||
func TestIgnitionHandler_MissingCtxProfile(t *testing.T) {
|
||||
h := ignitionHandler(&emptyStore{})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
@@ -67,7 +68,7 @@ func TestIgnitionHandler_MissingCtxSpec(t *testing.T) {
|
||||
|
||||
func TestIgnitionHandler_MissingIgnitionConfig(t *testing.T) {
|
||||
h := ignitionHandler(&emptyStore{})
|
||||
ctx := withSpec(context.Background(), testSpec)
|
||||
ctx := withProfile(context.Background(), testProfile)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(ctx, w, req)
|
||||
|
||||
@@ -32,13 +32,13 @@ func ipxeInspect() http.Handler {
|
||||
// requester.
|
||||
func ipxeHandler() ContextHandler {
|
||||
fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
spec, err := specFromContext(ctx)
|
||||
profile, err := profileFromContext(ctx)
|
||||
if err != nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = ipxeTemplate.Execute(&buf, spec.BootConfig)
|
||||
err = ipxeTemplate.Execute(&buf, profile.Boot)
|
||||
if err != nil {
|
||||
log.Errorf("error rendering template: %v", err)
|
||||
http.NotFound(w, req)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@@ -20,12 +21,12 @@ func TestIPXEInspect(t *testing.T) {
|
||||
|
||||
func TestIPXEHandler(t *testing.T) {
|
||||
h := ipxeHandler()
|
||||
ctx := withSpec(context.Background(), testSpec)
|
||||
ctx := withProfile(context.Background(), testProfile)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(ctx, w, req)
|
||||
// assert that:
|
||||
// - the Spec's boot config is rendered as an iPXE script
|
||||
// - the Profile's NetBoot config is rendered as an iPXE script
|
||||
expectedScript := `#!ipxe
|
||||
kernel /image/kernel a=b c
|
||||
initrd /image/initrd_a /image/initrd_b
|
||||
@@ -35,7 +36,7 @@ boot
|
||||
assert.Equal(t, expectedScript, w.Body.String())
|
||||
}
|
||||
|
||||
func TestIPXEHandler_MissingCtxSpec(t *testing.T) {
|
||||
func TestIPXEHandler_MissingCtxProfile(t *testing.T) {
|
||||
h := ipxeHandler()
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
@@ -45,8 +46,8 @@ func TestIPXEHandler_MissingCtxSpec(t *testing.T) {
|
||||
|
||||
func TestIPXEHandler_RenderTemplateError(t *testing.T) {
|
||||
h := ipxeHandler()
|
||||
// a Spec with nil BootConfig forces a template.Execute error
|
||||
ctx := withSpec(context.Background(), &Spec{BootConfig: nil})
|
||||
// a Profile with nil NetBoot forces a template.Execute error
|
||||
ctx := withProfile(context.Background(), &storagepb.Profile{Boot: nil})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(ctx, w, req)
|
||||
@@ -55,7 +56,7 @@ func TestIPXEHandler_RenderTemplateError(t *testing.T) {
|
||||
|
||||
func TestIPXEHandler_WriteError(t *testing.T) {
|
||||
h := ipxeHandler()
|
||||
ctx := withSpec(context.Background(), testSpec)
|
||||
ctx := withProfile(context.Background(), testProfile)
|
||||
w := NewUnwriteableResponseWriter()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(ctx, w, req)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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 map[string]string) bool {
|
||||
for key, val := range r {
|
||||
if labels == nil || labels[key] != val {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r RequirementSet) String() string {
|
||||
requirements := make([]string, 0, len(r))
|
||||
for key, value := range r {
|
||||
requirements = append(requirements, key+"="+value)
|
||||
}
|
||||
// sort by "key=value" pairs for a deterministic ordering
|
||||
sort.StringSlice(requirements).Sort()
|
||||
return strings.Join(requirements, ",")
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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)
|
||||
assert.Equal(t, c.expected, r.Matches(c.labels))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -18,7 +19,15 @@ func metadataHandler() ContextHandler {
|
||||
return
|
||||
}
|
||||
w.Header().Set(contentType, plainContentType)
|
||||
for key, value := range group.Metadata {
|
||||
|
||||
var data map[string]interface{}
|
||||
err = json.Unmarshal(group.Metadata, &data)
|
||||
if err != nil {
|
||||
log.Error("error unmarshalling metadata")
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
for key, value := range data {
|
||||
fmt.Fprintf(w, "%s=%s\n", strings.ToUpper(key), value)
|
||||
}
|
||||
attrs := labelsFromRequest(req)
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
|
||||
func TestMetadataHandler(t *testing.T) {
|
||||
h := metadataHandler()
|
||||
ctx := withGroup(context.Background(), &testGroup)
|
||||
ctx := withGroup(context.Background(), testGroup)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/?uuid=a1b2c3d4&mac="+nonNormalizedMACStr, nil)
|
||||
req, _ := http.NewRequest("GET", "/?uuid=a1b2c3d4&mac="+validMACStr, nil)
|
||||
h.ServeHTTP(ctx, w, req)
|
||||
// assert that:
|
||||
// - the Group's custom metadata is
|
||||
|
||||
@@ -3,12 +3,14 @@ package api
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage"
|
||||
)
|
||||
|
||||
// pixiecoreHandler returns a handler that renders the boot config JSON for
|
||||
// the requester, to implement the Pixiecore API specification.
|
||||
// https://github.com/danderson/pixiecore/blob/master/README.api.md
|
||||
func pixiecoreHandler(gr *groupsResource, store Store) http.Handler {
|
||||
func pixiecoreHandler(gr *groupsResource, store storage.Store) http.Handler {
|
||||
fn := func(w http.ResponseWriter, req *http.Request) {
|
||||
macAddr, err := parseMAC(filepath.Base(req.URL.Path))
|
||||
if err != nil {
|
||||
@@ -22,12 +24,12 @@ func pixiecoreHandler(gr *groupsResource, store Store) http.Handler {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
spec, err := store.Spec(group.Spec)
|
||||
profile, err := store.ProfileGet(group.Profile)
|
||||
if err != nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
renderJSON(w, spec.BootConfig)
|
||||
renderJSON(w, profile.Boot)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
@@ -5,21 +5,22 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPixiecoreHandler(t *testing.T) {
|
||||
store := &fixedStore{
|
||||
Groups: []Group{testGroupWithMAC},
|
||||
Specs: map[string]*Spec{testGroupWithMAC.Spec: testSpec},
|
||||
Groups: map[string]*storagepb.Group{testGroupWithMAC.Id: testGroupWithMAC},
|
||||
Profiles: map[string]*storagepb.Profile{testGroupWithMAC.Profile: testProfile},
|
||||
}
|
||||
h := pixiecoreHandler(newGroupsResource(store), store)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/"+validMACStr, nil)
|
||||
h.ServeHTTP(w, req)
|
||||
// assert that:
|
||||
// - MAC address argument is used for Spec matching
|
||||
// - the Spec's boot config is rendered as Pixiecore JSON
|
||||
// - MAC address argument is used for Group matching
|
||||
// - the Profile's NetBoot config is rendered as Pixiecore JSON
|
||||
expectedJSON := `{"kernel":"/image/kernel","initrd":["/image/initrd_a","/image/initrd_b"],"cmdline":{"a":"b","c":""}}`
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, jsonContentType, w.HeaderMap.Get(contentType))
|
||||
@@ -43,9 +44,9 @@ func TestPixiecoreHandler_NoMatchingGroup(t *testing.T) {
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestPixiecoreHandler_NoMatchingSpec(t *testing.T) {
|
||||
func TestPixiecoreHandler_NoMatchingProfile(t *testing.T) {
|
||||
store := &fixedStore{
|
||||
Groups: []Group{testGroupWithMAC},
|
||||
Groups: map[string]*storagepb.Group{testGroup.Id: testGroup},
|
||||
}
|
||||
h := pixiecoreHandler(newGroupsResource(store), &emptyStore{})
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/sign"
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
)
|
||||
|
||||
@@ -17,7 +18,7 @@ var log = capnslog.NewPackageLogger("github.com/coreos/coreos-baremetal/bootcfg"
|
||||
// Config configures the api Server.
|
||||
type Config struct {
|
||||
// Store for configs
|
||||
Store Store
|
||||
Store storage.Store
|
||||
// Path to static assets
|
||||
AssetsPath string
|
||||
// config signers (.sig and .asc)
|
||||
@@ -27,7 +28,7 @@ type Config struct {
|
||||
|
||||
// Server serves boot and provisioning configs to machines.
|
||||
type Server struct {
|
||||
store Store
|
||||
store storage.Store
|
||||
assetsPath string
|
||||
signer sign.Signer
|
||||
armoredSigner sign.Signer
|
||||
@@ -46,22 +47,20 @@ func NewServer(config *Config) *Server {
|
||||
// HTTPHandler returns a HTTP handler for the server.
|
||||
func (s *Server) HTTPHandler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
// API Resources
|
||||
newSpecResource(mux, "/spec/", s.store)
|
||||
gr := newGroupsResource(s.store)
|
||||
|
||||
// Endpoints
|
||||
mux.Handle("/grub", logRequests(NewHandler(gr.matchSpecHandler(grubHandler()))))
|
||||
// Boot via GRUB
|
||||
mux.Handle("/grub", logRequests(NewHandler(gr.matchProfileHandler(grubHandler()))))
|
||||
// Boot via iPXE
|
||||
mux.Handle("/boot.ipxe", logRequests(ipxeInspect()))
|
||||
mux.Handle("/boot.ipxe.0", logRequests(ipxeInspect()))
|
||||
mux.Handle("/ipxe", logRequests(NewHandler(gr.matchSpecHandler(ipxeHandler()))))
|
||||
mux.Handle("/ipxe", logRequests(NewHandler(gr.matchProfileHandler(ipxeHandler()))))
|
||||
// Boot via Pixiecore
|
||||
mux.Handle("/pixiecore/v1/boot/", logRequests(pixiecoreHandler(gr, s.store)))
|
||||
// cloud configs
|
||||
mux.Handle("/cloud", logRequests(NewHandler(gr.matchGroupHandler(cloudHandler(s.store)))))
|
||||
// ignition configs
|
||||
// Ignition Config
|
||||
mux.Handle("/ignition", logRequests(NewHandler(gr.matchGroupHandler(ignitionHandler(s.store)))))
|
||||
// Cloud-Config
|
||||
mux.Handle("/cloud", logRequests(NewHandler(gr.matchGroupHandler(cloudHandler(s.store)))))
|
||||
// metadata
|
||||
mux.Handle("/metadata", logRequests(NewHandler(gr.matchGroupHandler(metadataHandler()))))
|
||||
|
||||
@@ -70,26 +69,26 @@ func (s *Server) HTTPHandler() http.Handler {
|
||||
signerChain := func(next http.Handler) http.Handler {
|
||||
return logRequests(sign.SignatureHandler(s.signer, next))
|
||||
}
|
||||
mux.Handle("/grub.sig", signerChain(NewHandler(gr.matchSpecHandler(grubHandler()))))
|
||||
mux.Handle("/grub.sig", signerChain(NewHandler(gr.matchProfileHandler(grubHandler()))))
|
||||
mux.Handle("/boot.ipxe.sig", signerChain(ipxeInspect()))
|
||||
mux.Handle("/boot.ipxe.0.sig", signerChain(ipxeInspect()))
|
||||
mux.Handle("/ipxe.sig", signerChain(NewHandler(gr.matchSpecHandler(ipxeHandler()))))
|
||||
mux.Handle("/ipxe.sig", signerChain(NewHandler(gr.matchProfileHandler(ipxeHandler()))))
|
||||
mux.Handle("/pixiecore/v1/boot.sig/", signerChain(pixiecoreHandler(gr, s.store)))
|
||||
mux.Handle("/cloud.sig", signerChain(NewHandler(gr.matchGroupHandler(cloudHandler(s.store)))))
|
||||
mux.Handle("/ignition.sig", signerChain(NewHandler(gr.matchGroupHandler(ignitionHandler(s.store)))))
|
||||
mux.Handle("/cloud.sig", signerChain(NewHandler(gr.matchGroupHandler(cloudHandler(s.store)))))
|
||||
mux.Handle("/metadata.sig", signerChain(NewHandler(gr.matchGroupHandler(metadataHandler()))))
|
||||
}
|
||||
if s.armoredSigner != nil {
|
||||
signerChain := func(next http.Handler) http.Handler {
|
||||
return logRequests(sign.SignatureHandler(s.armoredSigner, next))
|
||||
}
|
||||
mux.Handle("/grub.asc", signerChain(NewHandler(gr.matchSpecHandler(grubHandler()))))
|
||||
mux.Handle("/grub.asc", signerChain(NewHandler(gr.matchProfileHandler(grubHandler()))))
|
||||
mux.Handle("/boot.ipxe.asc", signerChain(ipxeInspect()))
|
||||
mux.Handle("/boot.ipxe.0.asc", signerChain(ipxeInspect()))
|
||||
mux.Handle("/ipxe.asc", signerChain(NewHandler(gr.matchSpecHandler(ipxeHandler()))))
|
||||
mux.Handle("/ipxe.asc", signerChain(NewHandler(gr.matchProfileHandler(ipxeHandler()))))
|
||||
mux.Handle("/pixiecore/v1/boot.asc/", signerChain(pixiecoreHandler(gr, s.store)))
|
||||
mux.Handle("/cloud.asc", signerChain(NewHandler(gr.matchGroupHandler(cloudHandler(s.store)))))
|
||||
mux.Handle("/ignition.asc", signerChain(NewHandler(gr.matchGroupHandler(ignitionHandler(s.store)))))
|
||||
mux.Handle("/cloud.asc", signerChain(NewHandler(gr.matchGroupHandler(cloudHandler(s.store)))))
|
||||
mux.Handle("/metadata.asc", signerChain(NewHandler(gr.matchGroupHandler(metadataHandler()))))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestServerSpecRoute(t *testing.T) {
|
||||
store := &fixedStore{
|
||||
Specs: map[string]*Spec{"g1h2i3j4": testSpec},
|
||||
}
|
||||
h := NewServer(&Config{Store: store}).HTTPHandler()
|
||||
req, _ := http.NewRequest("GET", "/spec/g1h2i3j4", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
// assert that:
|
||||
// - spec is rendered as JSON
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, jsonContentType, w.HeaderMap.Get(contentType))
|
||||
assert.Equal(t, expectedSpecJSON, w.Body.String())
|
||||
}
|
||||
|
||||
func TestServerSpecRoute_WrongMethod(t *testing.T) {
|
||||
h := NewServer(&Config{}).HTTPHandler()
|
||||
req, _ := http.NewRequest("POST", "/spec/g1h2i3j4", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
||||
assert.Equal(t, "only HTTP GET is supported\n", w.Body.String())
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Spec is a named group of configs.
|
||||
type Spec struct {
|
||||
// spec identifier
|
||||
ID string `json:"id"`
|
||||
// boot kernel, initrd, and kernel options
|
||||
BootConfig *BootConfig `json:"boot"`
|
||||
// cloud config id
|
||||
CloudConfig string `json:"cloud_id"`
|
||||
// ignition config id
|
||||
IgnitionConfig string `json:"ignition_id"`
|
||||
}
|
||||
|
||||
// specResource serves Spec resources by id.
|
||||
type specResource struct {
|
||||
store Store
|
||||
}
|
||||
|
||||
func newSpecResource(mux *http.ServeMux, pattern string, store Store) {
|
||||
gr := &specResource{
|
||||
store: store,
|
||||
}
|
||||
mux.Handle(pattern, logRequests(requireGET(gr)))
|
||||
}
|
||||
|
||||
func (r *specResource) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
id := filepath.Base(req.URL.Path)
|
||||
spec, err := r.store.Spec(id)
|
||||
if err != nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
renderJSON(w, spec)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
// testSpec specifies a named group of configs for testing purposes.
|
||||
testSpec = &Spec{
|
||||
ID: "g1h2i3j4",
|
||||
BootConfig: &BootConfig{
|
||||
Kernel: "/image/kernel",
|
||||
Initrd: []string{"/image/initrd_a", "/image/initrd_b"},
|
||||
Cmdline: map[string]interface{}{
|
||||
"a": "b",
|
||||
"c": "",
|
||||
},
|
||||
},
|
||||
CloudConfig: "cloud-config.yml",
|
||||
IgnitionConfig: "ignition.json",
|
||||
}
|
||||
testSpecWithIgnitionYAML = &Spec{
|
||||
ID: "g1h2i3j4",
|
||||
IgnitionConfig: "ignition.yaml",
|
||||
}
|
||||
emptySpec = &Spec{
|
||||
ID: "empty",
|
||||
}
|
||||
expectedSpecJSON = `{"id":"g1h2i3j4","boot":{"kernel":"/image/kernel","initrd":["/image/initrd_a","/image/initrd_b"],"cmdline":{"a":"b","c":""}},"cloud_id":"cloud-config.yml","ignition_id":"ignition.json"}`
|
||||
)
|
||||
|
||||
func TestSpecHandler(t *testing.T) {
|
||||
store := &fixedStore{
|
||||
Specs: map[string]*Spec{"g1h2i3j4": testSpec},
|
||||
}
|
||||
h := specResource{store: store}
|
||||
req, _ := http.NewRequest("GET", "/g1h2i3j4", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
// assert that:
|
||||
// - spec is rendered as JSON
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, jsonContentType, w.HeaderMap.Get(contentType))
|
||||
assert.Equal(t, expectedSpecJSON, w.Body.String())
|
||||
}
|
||||
|
||||
func TestSpecHandler_MissingSpec(t *testing.T) {
|
||||
store := &emptyStore{}
|
||||
h := specResource{store}
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Store provides Group, Spec, Ignition, and Cloud config resources.
|
||||
type Store interface {
|
||||
BootstrapGroups([]Group) error
|
||||
ListGroups() ([]Group, error)
|
||||
|
||||
Spec(id string) (*Spec, error)
|
||||
CloudConfig(id string) (string, error)
|
||||
IgnitionConfig(id string) (string, error)
|
||||
}
|
||||
|
||||
// fileStore provides configs from an http.Filesystem.
|
||||
type fileStore struct {
|
||||
root http.FileSystem
|
||||
groups []Group
|
||||
}
|
||||
|
||||
// NewFileStore returns a Store backed by a filesystem directory.
|
||||
func NewFileStore(root http.FileSystem) Store {
|
||||
return &fileStore{
|
||||
root: root,
|
||||
}
|
||||
}
|
||||
|
||||
// BootstrapGroups loads an initial collection of groups.
|
||||
func (s *fileStore) BootstrapGroups(groups []Group) error {
|
||||
s.groups = groups
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListGroups returns the list of groups with matchers.
|
||||
func (s *fileStore) ListGroups() ([]Group, error) {
|
||||
return s.groups, nil
|
||||
}
|
||||
|
||||
// Spec returns the Spec with the given id.
|
||||
func (s *fileStore) Spec(id string) (*Spec, error) {
|
||||
file, err := openFile(s.root, filepath.Join("specs", id, "spec.json"))
|
||||
if err != nil {
|
||||
log.Debugf("no spec %s", id)
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
spec := new(Spec)
|
||||
err = json.NewDecoder(file).Decode(spec)
|
||||
if err != nil {
|
||||
log.Errorf("error decoding spec: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// CloudConfig returns the cloud config with the given id.
|
||||
func (s *fileStore) CloudConfig(id string) (string, error) {
|
||||
file, err := openFile(s.root, filepath.Join("cloud", id))
|
||||
if err != nil {
|
||||
log.Debugf("no cloud config %s", id)
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
b, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
log.Errorf("error reading cloud config: %s", err)
|
||||
return "", err
|
||||
}
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// IgnitionConfig returns the ignition template with the given id.
|
||||
func (s *fileStore) IgnitionConfig(id string) (string, error) {
|
||||
file, err := openFile(s.root, filepath.Join("ignition", id))
|
||||
if err != nil {
|
||||
log.Debugf("no ignition config %s", id)
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
b, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
log.Errorf("error reading ignition config: %s", err)
|
||||
return "", err
|
||||
}
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// openFile attempts to open the file within the specified Filesystem. If
|
||||
// successful, the http.File is returned and must be closed by the caller.
|
||||
// Otherwise, the path was not a regular file that could be opened and an
|
||||
// error is returned.
|
||||
func openFile(fs http.FileSystem, path string) (http.File, error) {
|
||||
file, err := fs.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, err
|
||||
}
|
||||
if info.Mode().IsRegular() {
|
||||
return file, nil
|
||||
}
|
||||
file.Close()
|
||||
return nil, fmt.Errorf("%s is not a file on the given filesystem", path)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// fixedStore provides fixed Group, Spec, Ignition, and Cloud config resources
|
||||
// for testing.
|
||||
type fixedStore struct {
|
||||
Groups []Group
|
||||
Specs map[string]*Spec
|
||||
CloudConfigs map[string]string
|
||||
IgnitionConfigs map[string]string
|
||||
}
|
||||
|
||||
func (s *fixedStore) Spec(id string) (*Spec, error) {
|
||||
if spec, present := s.Specs[id]; present {
|
||||
return spec, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no spec %s", id)
|
||||
}
|
||||
|
||||
func (s *fixedStore) BootstrapGroups(groups []Group) error {
|
||||
s.Groups = groups
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fixedStore) ListGroups() ([]Group, error) {
|
||||
return s.Groups, nil
|
||||
}
|
||||
|
||||
func (s *fixedStore) CloudConfig(id string) (string, error) {
|
||||
if config, present := s.CloudConfigs[id]; present {
|
||||
return config, nil
|
||||
}
|
||||
return "", fmt.Errorf("no cloud config %s", id)
|
||||
}
|
||||
|
||||
func (s *fixedStore) IgnitionConfig(id string) (string, error) {
|
||||
if config, present := s.IgnitionConfigs[id]; present {
|
||||
return config, nil
|
||||
}
|
||||
return "", fmt.Errorf("no ignition config %s", id)
|
||||
}
|
||||
|
||||
type emptyStore struct{}
|
||||
|
||||
func (s *emptyStore) Spec(id string) (*Spec, error) {
|
||||
return nil, fmt.Errorf("no group config %s", id)
|
||||
}
|
||||
|
||||
func (s *emptyStore) BootstrapGroups(groups []Group) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *emptyStore) ListGroups() (groups []Group, err error) {
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (s emptyStore) CloudConfig(id string) (string, error) {
|
||||
return "", fmt.Errorf("no cloud config %s", id)
|
||||
}
|
||||
|
||||
func (s emptyStore) IgnitionConfig(id string) (string, error) {
|
||||
return "", fmt.Errorf("no ignition config %s", id)
|
||||
}
|
||||
127
bootcfg/api/test_fixtures.go
Normal file
127
bootcfg/api/test_fixtures.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage"
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb"
|
||||
)
|
||||
|
||||
var (
|
||||
validMACStr = "52:da:00:89:d8:10"
|
||||
|
||||
testProfile = &storagepb.Profile{
|
||||
Id: "g1h2i3j4",
|
||||
Boot: &storagepb.NetBoot{
|
||||
Kernel: "/image/kernel",
|
||||
Initrd: []string{"/image/initrd_a", "/image/initrd_b"},
|
||||
Cmdline: map[string]string{
|
||||
"a": "b",
|
||||
"c": "",
|
||||
},
|
||||
},
|
||||
CloudId: "cloud-config.yml",
|
||||
IgnitionId: "ignition.json",
|
||||
}
|
||||
|
||||
testProfileIgnitionYAML = &storagepb.Profile{
|
||||
Id: "g1h2i3j4",
|
||||
IgnitionId: "ignition.yaml",
|
||||
}
|
||||
|
||||
testGroup = &storagepb.Group{
|
||||
Id: "test-group",
|
||||
Name: "test group",
|
||||
Profile: "g1h2i3j4",
|
||||
Requirements: map[string]string{"uuid": "a1b2c3d4"},
|
||||
Metadata: []byte(`{"k8s_version":"v1.1.2","service_name":"etcd2","pod_network": "10.2.0.0/16"}`),
|
||||
}
|
||||
|
||||
testGroupWithMAC = &storagepb.Group{
|
||||
Id: "test-group",
|
||||
Name: "test group",
|
||||
Profile: "g1h2i3j4",
|
||||
Requirements: map[string]string{"mac": validMACStr},
|
||||
}
|
||||
)
|
||||
|
||||
type fixedStore struct {
|
||||
Groups map[string]*storagepb.Group
|
||||
Profiles map[string]*storagepb.Profile
|
||||
IgnitionConfigs map[string]string
|
||||
CloudConfigs map[string]string
|
||||
}
|
||||
|
||||
func (s *fixedStore) GroupGet(id string) (*storagepb.Group, error) {
|
||||
if group, present := s.Groups[id]; present {
|
||||
return group, nil
|
||||
}
|
||||
return nil, storage.ErrGroupNotFound
|
||||
}
|
||||
|
||||
func (s *fixedStore) GroupList() ([]*storagepb.Group, error) {
|
||||
groups := make([]*storagepb.Group, len(s.Groups))
|
||||
i := 0
|
||||
for _, g := range s.Groups {
|
||||
groups[i] = g
|
||||
i++
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (s *fixedStore) ProfileGet(id string) (*storagepb.Profile, error) {
|
||||
if profile, present := s.Profiles[id]; present {
|
||||
return profile, nil
|
||||
}
|
||||
return nil, storage.ErrProfileNotFound
|
||||
}
|
||||
|
||||
func (s *fixedStore) ProfileList() ([]*storagepb.Profile, error) {
|
||||
profiles := make([]*storagepb.Profile, len(s.Profiles))
|
||||
i := 0
|
||||
for _, p := range s.Profiles {
|
||||
profiles[i] = p
|
||||
i++
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (s *fixedStore) IgnitionGet(id string) (string, error) {
|
||||
if config, present := s.IgnitionConfigs[id]; present {
|
||||
return config, nil
|
||||
}
|
||||
return "", fmt.Errorf("no Ignition Config %s", id)
|
||||
}
|
||||
|
||||
func (s *fixedStore) CloudGet(id string) (string, error) {
|
||||
if config, present := s.CloudConfigs[id]; present {
|
||||
return config, nil
|
||||
}
|
||||
return "", fmt.Errorf("no Cloud Config %s", id)
|
||||
}
|
||||
|
||||
type emptyStore struct{}
|
||||
|
||||
func (s *emptyStore) GroupGet(id string) (*storagepb.Group, error) {
|
||||
return nil, storage.ErrGroupNotFound
|
||||
}
|
||||
|
||||
func (s *emptyStore) GroupList() (groups []*storagepb.Group, err error) {
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (s *emptyStore) ProfileGet(id string) (*storagepb.Profile, error) {
|
||||
return nil, storage.ErrProfileNotFound
|
||||
}
|
||||
|
||||
func (s *emptyStore) ProfileList() (profiles []*storagepb.Profile, err error) {
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (s *emptyStore) IgnitionGet(id string) (string, error) {
|
||||
return "", fmt.Errorf("no Ignition Config %s", id)
|
||||
}
|
||||
|
||||
func (s *emptyStore) CloudGet(id string) (string, error) {
|
||||
return "", fmt.Errorf("no Cloud Config %s", id)
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/api"
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/satori/go.uuid"
|
||||
@@ -28,15 +27,28 @@ var (
|
||||
|
||||
var log = capnslog.NewPackageLogger("github.com/coreos/coreos-baremetal/bootcfg", "config")
|
||||
|
||||
// Config is a user defined matching of machines to specifications.
|
||||
// Config is a user defined matching of machine groups to profiles.
|
||||
type Config struct {
|
||||
APIVersion string `yaml:"api_version"`
|
||||
// allow YAML source for Groups
|
||||
YAMLGroups []api.Group `yaml:"groups"`
|
||||
YAMLGroups []Group `yaml:"groups"`
|
||||
// populate protobuf Groups at parse
|
||||
Groups []*storagepb.Group `yaml:"-"`
|
||||
}
|
||||
|
||||
// A Group associates machines with required tags with a Profile and
|
||||
// metadata. Zero or more machines may match to a machine Group.
|
||||
type Group struct {
|
||||
// Human readable name
|
||||
Name string `yaml:"name"`
|
||||
// Profile id
|
||||
Profile string `yaml:"spec"`
|
||||
// tags required to match the group
|
||||
Requirements map[string]string `yaml:"require"`
|
||||
// Metadata (with restrictions)
|
||||
Metadata map[string]interface{} `yaml:"metadata"`
|
||||
}
|
||||
|
||||
// LoadConfig opens a file and parses YAML data to returns a Config.
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
data, err := ioutil.ReadFile(os.ExpandEnv(path))
|
||||
@@ -58,8 +70,8 @@ func ParseConfig(data []byte) (*Config, error) {
|
||||
for _, ygroup := range config.YAMLGroups {
|
||||
group := &storagepb.Group{
|
||||
Name: ygroup.Name,
|
||||
Profile: ygroup.Spec,
|
||||
Requirements: normalizeMatchers(ygroup.Matcher),
|
||||
Profile: ygroup.Profile,
|
||||
Requirements: normalizeMatchers(ygroup.Requirements),
|
||||
}
|
||||
// Id: Generate a random UUID or use the name
|
||||
if ygroup.Name == "" {
|
||||
@@ -159,7 +171,7 @@ func (c *Config) validate() error {
|
||||
return ErrIncorrectVersion
|
||||
}
|
||||
for _, group := range c.YAMLGroups {
|
||||
for key, val := range group.Matcher {
|
||||
for key, val := range group.Requirements {
|
||||
switch strings.ToLower(key) {
|
||||
case "mac":
|
||||
macAddr, err := net.ParseMAC(val)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/api"
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -88,35 +87,35 @@ func TestValidate(t *testing.T) {
|
||||
}
|
||||
invalidMAC := &Config{
|
||||
APIVersion: "v1alpha1",
|
||||
YAMLGroups: []api.Group{
|
||||
api.Group{
|
||||
Matcher: api.RequirementSet(map[string]string{
|
||||
YAMLGroups: []Group{
|
||||
Group{
|
||||
Requirements: map[string]string{
|
||||
"mac": "?:?:?:?",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
nonNormalizedMAC := &Config{
|
||||
APIVersion: "v1alpha1",
|
||||
YAMLGroups: []api.Group{
|
||||
api.Group{
|
||||
Matcher: api.RequirementSet(map[string]string{
|
||||
YAMLGroups: []Group{
|
||||
Group{
|
||||
Requirements: map[string]string{
|
||||
"mac": "aB:Ab:3d:45:cD:10",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
validConfig := &Config{
|
||||
APIVersion: "v1alpha1",
|
||||
YAMLGroups: []api.Group{
|
||||
api.Group{
|
||||
Name: "node1",
|
||||
Spec: "worker",
|
||||
Matcher: api.RequirementSet(map[string]string{
|
||||
YAMLGroups: []Group{
|
||||
Group{
|
||||
Name: "node1",
|
||||
Profile: "worker",
|
||||
Requirements: map[string]string{
|
||||
"role": "worker",
|
||||
"region": "us-central1-a",
|
||||
"mac": "ab:ab:3d:45:cd:10",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ type Store interface {
|
||||
ProfileGet(id string) (*storagepb.Profile, error)
|
||||
// ProfileList lists all profiles.
|
||||
ProfileList() ([]*storagepb.Profile, error)
|
||||
// IgnitionGet gets an Ignition Config template by name.
|
||||
IgnitionGet(name string) (string, error)
|
||||
// CloudGet gets a Cloud-Config template by name.
|
||||
CloudGet(name string) (string, error)
|
||||
}
|
||||
|
||||
// Config initializes a fileStore.
|
||||
@@ -106,6 +110,36 @@ func (s *fileStore) ProfileList() ([]*storagepb.Profile, error) {
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// IgnitionGet gets an Ignition Config template by name.
|
||||
func (s *fileStore) IgnitionGet(id string) (string, error) {
|
||||
file, err := openFile(http.Dir(s.dir), filepath.Join("ignition", id))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// CloudGet gets a Cloud-Config template by name.
|
||||
func (s *fileStore) CloudGet(id string) (string, error) {
|
||||
file, err := openFile(http.Dir(s.dir), filepath.Join("cloud", id))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// openFile attempts to open the file within the specified Filesystem. If
|
||||
// successful, the http.File is returned and must be closed by the caller.
|
||||
// Otherwise, the path was not a regular file that could be opened and an
|
||||
|
||||
@@ -32,7 +32,7 @@ func (g *Group) requirementString() string {
|
||||
// sorted order by increasing number of Requirements, then by sorted key/value
|
||||
// strings. For example, a Group with Requirements {a:b, c:d} should be ordered
|
||||
// after one with {a:b} and before one with {a:d, c:d}.
|
||||
type ByReqs []Group
|
||||
type ByReqs []*Group
|
||||
|
||||
func (groups ByReqs) Len() int {
|
||||
return len(groups)
|
||||
|
||||
@@ -8,16 +8,15 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
validMACStr = "52:da:00:89:d8:10"
|
||||
testGroup = Group{
|
||||
testGroup = &Group{
|
||||
Name: "test group",
|
||||
Profile: "g1h2i3j4",
|
||||
Requirements: map[string]string{
|
||||
"uuid": "a1b2c3d4",
|
||||
"mac": validMACStr,
|
||||
"mac": "52:da:00:89:d8:10",
|
||||
},
|
||||
}
|
||||
testGroupWithoutProfile = Group{
|
||||
testGroupWithoutProfile = &Group{
|
||||
Name: "test group without profile",
|
||||
Profile: "",
|
||||
Requirements: map[string]string{"uuid": "a1b2c3d4"},
|
||||
@@ -56,20 +55,20 @@ func TestRequirementString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGroupSort(t *testing.T) {
|
||||
oneCondition := Group{
|
||||
oneCondition := &Group{
|
||||
Name: "group with one requirement",
|
||||
Requirements: map[string]string{
|
||||
"region": "a",
|
||||
},
|
||||
}
|
||||
twoConditions := Group{
|
||||
twoConditions := &Group{
|
||||
Name: "group with two requirements",
|
||||
Requirements: map[string]string{
|
||||
"region": "a",
|
||||
"zone": "z",
|
||||
},
|
||||
}
|
||||
dualConditions := Group{
|
||||
dualConditions := &Group{
|
||||
Name: "group with two requirements",
|
||||
Requirements: map[string]string{
|
||||
"region": "b",
|
||||
@@ -77,12 +76,12 @@ func TestGroupSort(t *testing.T) {
|
||||
},
|
||||
}
|
||||
cases := []struct {
|
||||
input []Group
|
||||
expected []Group
|
||||
input []*Group
|
||||
expected []*Group
|
||||
}{
|
||||
{[]Group{oneCondition, dualConditions, twoConditions}, []Group{oneCondition, twoConditions, dualConditions}},
|
||||
{[]Group{twoConditions, dualConditions, oneCondition}, []Group{oneCondition, twoConditions, dualConditions}},
|
||||
{[]Group{testGroup, testGroupWithoutProfile, oneCondition, twoConditions, dualConditions}, []Group{oneCondition, testGroupWithoutProfile, testGroup, twoConditions, dualConditions}},
|
||||
{[]*Group{oneCondition, dualConditions, twoConditions}, []*Group{oneCondition, twoConditions, dualConditions}},
|
||||
{[]*Group{twoConditions, dualConditions, oneCondition}, []*Group{oneCondition, twoConditions, dualConditions}},
|
||||
{[]*Group{testGroup, testGroupWithoutProfile, oneCondition, twoConditions, dualConditions}, []*Group{oneCondition, testGroupWithoutProfile, testGroup, twoConditions, dualConditions}},
|
||||
}
|
||||
// assert that
|
||||
// - Group ordering is deterministic
|
||||
|
||||
@@ -6,7 +6,7 @@ message Group {
|
||||
string id = 1;
|
||||
// human readable name
|
||||
string name = 2;
|
||||
// profile id
|
||||
// Profile id
|
||||
string profile = 3;
|
||||
// tags required to match the group
|
||||
map<string, string> requirements = 4;
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/api"
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/config"
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/sign"
|
||||
"github.com/coreos/coreos-baremetal/bootcfg/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -84,9 +85,6 @@ func main() {
|
||||
capnslog.SetGlobalLogLevel(lvl)
|
||||
capnslog.SetFormatter(capnslog.NewPrettyFormatter(os.Stdout, false))
|
||||
|
||||
// storage
|
||||
store := api.NewFileStore(http.Dir(flags.dataPath))
|
||||
|
||||
// (optional) signing
|
||||
var signer, armoredSigner sign.Signer
|
||||
if flags.keyRingPath != "" {
|
||||
@@ -103,7 +101,12 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
store.BootstrapGroups(cfg.YAMLGroups)
|
||||
|
||||
// storage
|
||||
store := storage.NewFileStore(&storage.Config{
|
||||
Dir: flags.dataPath,
|
||||
Groups: cfg.Groups,
|
||||
})
|
||||
|
||||
// HTTP server
|
||||
config := &api.Config{
|
||||
|
||||
Reference in New Issue
Block a user