From 0d148581b99acaa1f08bb9c5f7e276f744715c70 Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Thu, 10 Mar 2016 18:07:10 -0800 Subject: [PATCH] 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 --- CHANGES.md | 1 + bootcfg/api/boot.go | 11 -- bootcfg/api/cloud.go | 21 ++-- bootcfg/api/cloud_test.go | 17 ++-- bootcfg/api/context.go | 29 +++--- bootcfg/api/context_test.go | 25 ++--- bootcfg/api/groups.go | 102 +++++++------------ bootcfg/api/groups_test.go | 111 +++------------------ bootcfg/api/grub.go | 4 +- bootcfg/api/http.go | 2 +- bootcfg/api/ignition.go | 25 +++-- bootcfg/api/ignition_test.go | 17 ++-- bootcfg/api/ipxe.go | 4 +- bootcfg/api/ipxe_test.go | 13 +-- bootcfg/api/matchers.go | 31 ------ bootcfg/api/matchers_test.go | 47 --------- bootcfg/api/metadata.go | 11 +- bootcfg/api/metadata_test.go | 4 +- bootcfg/api/pixiecore.go | 8 +- bootcfg/api/pixiecore_test.go | 13 +-- bootcfg/api/server.go | 31 +++--- bootcfg/api/server_test.go | 33 ------ bootcfg/api/spec.go | 40 -------- bootcfg/api/spec_test.go | 58 ----------- bootcfg/api/store.go | 114 --------------------- bootcfg/api/store_test.go | 66 ------------ bootcfg/api/test_fixtures.go | 127 ++++++++++++++++++++++++ bootcfg/config/config.go | 24 +++-- bootcfg/config/config_test.go | 29 +++--- bootcfg/storage/storage.go | 34 +++++++ bootcfg/storage/storagepb/group.go | 2 +- bootcfg/storage/storagepb/group_test.go | 23 ++--- bootcfg/storage/storagepb/storage.proto | 2 +- cmd/bootcfg/main.go | 11 +- 34 files changed, 389 insertions(+), 701 deletions(-) delete mode 100644 bootcfg/api/boot.go delete mode 100644 bootcfg/api/matchers.go delete mode 100644 bootcfg/api/matchers_test.go delete mode 100644 bootcfg/api/server_test.go delete mode 100644 bootcfg/api/spec.go delete mode 100644 bootcfg/api/spec_test.go delete mode 100644 bootcfg/api/store.go delete mode 100644 bootcfg/api/store_test.go create mode 100644 bootcfg/api/test_fixtures.go diff --git a/CHANGES.md b/CHANGES.md index 8b6ffc43..9ae14a84 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## Latest +* Remove HTTP `/spec/id` JSON endpoint * Add detached OpenPGP signature endpoints (`.sig`) ## v0.2.0 (2016-02-09) diff --git a/bootcfg/api/boot.go b/bootcfg/api/boot.go deleted file mode 100644 index a0edf8ef..00000000 --- a/bootcfg/api/boot.go +++ /dev/null @@ -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"` -} diff --git a/bootcfg/api/cloud.go b/bootcfg/api/cloud.go index d7ad850f..2eeff910 100644 --- a/bootcfg/api/cloud.go +++ b/bootcfg/api/cloud.go @@ -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 diff --git a/bootcfg/api/cloud_test.go b/bootcfg/api/cloud_test.go index 55a07931..b4631a52 100644 --- a/bootcfg/api/cloud_test.go +++ b/bootcfg/api/cloud_test.go @@ -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) diff --git a/bootcfg/api/context.go b/bootcfg/api/context.go index a8bca778..af80d829 100644 --- a/bootcfg/api/context.go +++ b/bootcfg/api/context.go @@ -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 } diff --git a/bootcfg/api/context_test.go b/bootcfg/api/context_test.go index 1610fb69..75ef4753 100644 --- a/bootcfg/api/context_test.go +++ b/bootcfg/api/context_test.go @@ -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) { diff --git a/bootcfg/api/groups.go b/bootcfg/api/groups.go index e84a7b23..3f302b34 100644 --- a/bootcfg/api/groups.go +++ b/bootcfg/api/groups.go @@ -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 diff --git a/bootcfg/api/groups_test.go b/bootcfg/api/groups_test.go index 4a48c0da..7de3db6c 100644 --- a/bootcfg/api/groups_test.go +++ b/bootcfg/api/groups_test.go @@ -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 { diff --git a/bootcfg/api/grub.go b/bootcfg/api/grub.go index b305c034..465078c9 100644 --- a/bootcfg/api/grub.go +++ b/bootcfg/api/grub.go @@ -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) diff --git a/bootcfg/api/http.go b/bootcfg/api/http.go index 379ac859..ffc93b42 100644 --- a/bootcfg/api/http.go +++ b/bootcfg/api/http.go @@ -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{} diff --git a/bootcfg/api/ignition.go b/bootcfg/api/ignition.go index 257bca21..1354877c 100644 --- a/bootcfg/api/ignition.go +++ b/bootcfg/api/ignition.go @@ -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) diff --git a/bootcfg/api/ignition_test.go b/bootcfg/api/ignition_test.go index 640670dc..c5fd6b11 100644 --- a/bootcfg/api/ignition_test.go +++ b/bootcfg/api/ignition_test.go @@ -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) diff --git a/bootcfg/api/ipxe.go b/bootcfg/api/ipxe.go index 3b262fb5..d7e9e8e8 100644 --- a/bootcfg/api/ipxe.go +++ b/bootcfg/api/ipxe.go @@ -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) diff --git a/bootcfg/api/ipxe_test.go b/bootcfg/api/ipxe_test.go index f3420c9b..52ba8d33 100644 --- a/bootcfg/api/ipxe_test.go +++ b/bootcfg/api/ipxe_test.go @@ -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) diff --git a/bootcfg/api/matchers.go b/bootcfg/api/matchers.go deleted file mode 100644 index 65173dbd..00000000 --- a/bootcfg/api/matchers.go +++ /dev/null @@ -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, ",") -} diff --git a/bootcfg/api/matchers_test.go b/bootcfg/api/matchers_test.go deleted file mode 100644 index 488df3ae..00000000 --- a/bootcfg/api/matchers_test.go +++ /dev/null @@ -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)) - } -} diff --git a/bootcfg/api/metadata.go b/bootcfg/api/metadata.go index 0b51ae35..b8c0d61f 100644 --- a/bootcfg/api/metadata.go +++ b/bootcfg/api/metadata.go @@ -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) diff --git a/bootcfg/api/metadata_test.go b/bootcfg/api/metadata_test.go index a8c29d0d..a5381c60 100644 --- a/bootcfg/api/metadata_test.go +++ b/bootcfg/api/metadata_test.go @@ -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 diff --git a/bootcfg/api/pixiecore.go b/bootcfg/api/pixiecore.go index 9416b230..b4eae6b8 100644 --- a/bootcfg/api/pixiecore.go +++ b/bootcfg/api/pixiecore.go @@ -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) } diff --git a/bootcfg/api/pixiecore_test.go b/bootcfg/api/pixiecore_test.go index a08fbd42..4e6f967f 100644 --- a/bootcfg/api/pixiecore_test.go +++ b/bootcfg/api/pixiecore_test.go @@ -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() diff --git a/bootcfg/api/server.go b/bootcfg/api/server.go index f904f39e..6013b976 100644 --- a/bootcfg/api/server.go +++ b/bootcfg/api/server.go @@ -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())))) } diff --git a/bootcfg/api/server_test.go b/bootcfg/api/server_test.go deleted file mode 100644 index de3c53ca..00000000 --- a/bootcfg/api/server_test.go +++ /dev/null @@ -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()) -} diff --git a/bootcfg/api/spec.go b/bootcfg/api/spec.go deleted file mode 100644 index 4b12c88c..00000000 --- a/bootcfg/api/spec.go +++ /dev/null @@ -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) -} diff --git a/bootcfg/api/spec_test.go b/bootcfg/api/spec_test.go deleted file mode 100644 index 0251e83b..00000000 --- a/bootcfg/api/spec_test.go +++ /dev/null @@ -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) -} diff --git a/bootcfg/api/store.go b/bootcfg/api/store.go deleted file mode 100644 index 6ae39b10..00000000 --- a/bootcfg/api/store.go +++ /dev/null @@ -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) -} diff --git a/bootcfg/api/store_test.go b/bootcfg/api/store_test.go deleted file mode 100644 index e6fe6389..00000000 --- a/bootcfg/api/store_test.go +++ /dev/null @@ -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) -} diff --git a/bootcfg/api/test_fixtures.go b/bootcfg/api/test_fixtures.go new file mode 100644 index 00000000..5bcbd487 --- /dev/null +++ b/bootcfg/api/test_fixtures.go @@ -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) +} diff --git a/bootcfg/config/config.go b/bootcfg/config/config.go index 70452ace..32a072d0 100644 --- a/bootcfg/config/config.go +++ b/bootcfg/config/config.go @@ -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) diff --git a/bootcfg/config/config_test.go b/bootcfg/config/config_test.go index 06af6637..75b1a3fe 100644 --- a/bootcfg/config/config_test.go +++ b/bootcfg/config/config_test.go @@ -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", - }), + }, }, }, } diff --git a/bootcfg/storage/storage.go b/bootcfg/storage/storage.go index d61d62fc..543e80e7 100644 --- a/bootcfg/storage/storage.go +++ b/bootcfg/storage/storage.go @@ -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 diff --git a/bootcfg/storage/storagepb/group.go b/bootcfg/storage/storagepb/group.go index 72c37f47..0d30dd01 100644 --- a/bootcfg/storage/storagepb/group.go +++ b/bootcfg/storage/storagepb/group.go @@ -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) diff --git a/bootcfg/storage/storagepb/group_test.go b/bootcfg/storage/storagepb/group_test.go index c10004af..5ea4c8e6 100644 --- a/bootcfg/storage/storagepb/group_test.go +++ b/bootcfg/storage/storagepb/group_test.go @@ -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 diff --git a/bootcfg/storage/storagepb/storage.proto b/bootcfg/storage/storagepb/storage.proto index d45419f3..532e0ae2 100644 --- a/bootcfg/storage/storagepb/storage.proto +++ b/bootcfg/storage/storagepb/storage.proto @@ -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 requirements = 4; diff --git a/cmd/bootcfg/main.go b/cmd/bootcfg/main.go index b41873e4..f8da5621 100644 --- a/cmd/bootcfg/main.go +++ b/cmd/bootcfg/main.go @@ -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{