From e0bfb3947b62fd17d7fb996c0cfd5bbdb46838d5 Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Mon, 28 Mar 2016 11:01:20 -0700 Subject: [PATCH] bootcfg/api,server: Refactor Group and Profile selection into server * Add gRPC SelectGroup and SelectProfile service endpoints * HTTP and gRPC servers should both allow labels to be resolved to the matching Group or Profile. Move logic to bootcfg core server. * Add gRPC wrapper selectServer to avoid cluttering core server with response types which are currently only useful for gRPC --- bootcfg/api/groups.go | 80 ---------- bootcfg/api/groups_test.go | 87 ----------- bootcfg/api/handlers.go | 39 +++++ bootcfg/api/handlers_test.go | 49 ++++++ bootcfg/api/http_test.go | 22 +-- bootcfg/api/pixiecore.go | 17 +- bootcfg/api/pixiecore_test.go | 27 ++-- bootcfg/api/server.go | 49 +++--- bootcfg/rpc/grpc.go | 1 + bootcfg/rpc/select.go | 30 ++++ bootcfg/server/server.go | 41 +++++ bootcfg/server/serverpb/rpc.pb.go | 240 ++++++++++++++++++++++++----- bootcfg/server/serverpb/rpc.proto | 27 +++- bootcfg/storage/storagepb/group.go | 2 +- 14 files changed, 450 insertions(+), 261 deletions(-) delete mode 100644 bootcfg/api/groups.go delete mode 100644 bootcfg/api/groups_test.go create mode 100644 bootcfg/rpc/select.go diff --git a/bootcfg/api/groups.go b/bootcfg/api/groups.go deleted file mode 100644 index 3f302b34..00000000 --- a/bootcfg/api/groups.go +++ /dev/null @@ -1,80 +0,0 @@ -package api - -import ( - "errors" - "net/http" - "sort" - - "github.com/coreos/coreos-baremetal/bootcfg/storage" - "github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb" - "golang.org/x/net/context" -) - -var ( - errNoMatchingGroup = errors.New("api: No matching Group") -) - -type groupsResource struct { - store storage.Store -} - -func newGroupsResource(store storage.Store) *groupsResource { - return &groupsResource{ - store: store, - } -} - -// 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) - // match machine request - group, err := gr.findMatch(attrs) - if err == nil { - // add the Group to the ctx for next handler - ctx = withGroup(ctx, group) - } - next.ServeHTTP(ctx, w, req) - } - 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) (*storagepb.Group, error) { - groups, err := gr.store.GroupList() - if err != nil { - return nil, err - } - sort.Sort(sort.Reverse(storagepb.ByReqs(groups))) - for _, group := range groups { - if group.Matches(labels) { - return group, nil - } - } - return nil, errNoMatchingGroup -} diff --git a/bootcfg/api/groups_test.go b/bootcfg/api/groups_test.go deleted file mode 100644 index 7de3db6c..00000000 --- a/bootcfg/api/groups_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package api - -import ( - "fmt" - "net/http" - "net/http/httptest" - "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" -) - -func TestNewGroupsResource(t *testing.T) { - store := &fixedStore{} - gr := newGroupsResource(store) - assert.Equal(t, store, gr.store) -} - -func TestGroupsResource_MatchProfileHandler(t *testing.T) { - store := &fixedStore{ - 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) { - profile, err := profileFromContext(ctx) - assert.Nil(t, err) - 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 Profile is found by id and added to the context - // - next handler is called - h := gr.matchProfileHandler(ContextHandlerFunc(next)) - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "?uuid=a1b2c3d4", nil) - h.ServeHTTP(context.Background(), w, req) - assert.Equal(t, "next handler called", w.Body.String()) -} - -func TestGroupsResource_MatchGroupHandler(t *testing.T) { - store := &fixedStore{ - 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) - fmt.Fprintf(w, "next handler called") - } - // assert that: - // - request arguments are used to match uuid=a1b2c3d4 -> testGroup - // - next handler is called - h := gr.matchGroupHandler(ContextHandlerFunc(next)) - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "?uuid=a1b2c3d4", nil) - h.ServeHTTP(context.Background(), w, req) - assert.Equal(t, "next handler called", w.Body.String()) -} - -func TestGroupsResource_FindMatch(t *testing.T) { - store := &fixedStore{ - Groups: map[string]*storagepb.Group{testGroup.Id: testGroup}, - } - cases := []struct { - store storage.Store - labels map[string]string - expectedGroup *storagepb.Group - expectedErr error - }{ - {store, map[string]string{"uuid": "a1b2c3d4"}, testGroup, nil}, - {store, nil, nil, errNoMatchingGroup}, - // no groups in the store - {&emptyStore{}, map[string]string{"a": "b"}, nil, errNoMatchingGroup}, - } - - for _, c := range cases { - gr := newGroupsResource(c.store) - group, err := gr.findMatch(c.labels) - assert.Equal(t, c.expectedGroup, group) - assert.Equal(t, c.expectedErr, err) - } -} diff --git a/bootcfg/api/handlers.go b/bootcfg/api/handlers.go index 65a4b97f..54ebc6d6 100644 --- a/bootcfg/api/handlers.go +++ b/bootcfg/api/handlers.go @@ -2,6 +2,11 @@ package api import ( "net/http" + + "golang.org/x/net/context" + + "github.com/coreos/coreos-baremetal/bootcfg/server" + pb "github.com/coreos/coreos-baremetal/bootcfg/server/serverpb" ) // requireGET requires requests to be an HTTP GET. Otherwise, it responds with @@ -25,3 +30,37 @@ func logRequests(next http.Handler) http.Handler { } return http.HandlerFunc(fn) } + +// selectGroup selects the Group whose selectors match the query parameters, +// adds the Group to the ctx, and calls the next handler. The next handler +// should handle a missing Group. +func selectGroup(srv server.Server, next ContextHandler) ContextHandler { + fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) { + attrs := labelsFromRequest(req) + // match machine request + group, err := srv.SelectGroup(ctx, &pb.SelectGroupRequest{Labels: attrs}) + if err == nil { + // add the Group to the ctx for next handler + ctx = withGroup(ctx, group) + } + next.ServeHTTP(ctx, w, req) + } + return ContextHandlerFunc(fn) +} + +// selectProfile selects the Profile for the given query parameters, adds the +// Profile to the ctx, and calls the next handler. The next handler should +// handle a missing profile. +func selectProfile(srv server.Server, next ContextHandler) ContextHandler { + fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) { + attrs := labelsFromRequest(req) + // match machine request + profile, err := srv.SelectProfile(ctx, &pb.SelectProfileRequest{Labels: attrs}) + if err == nil { + // add the Profile to the ctx for the next handler + ctx = withProfile(ctx, profile) + } + next.ServeHTTP(ctx, w, req) + } + return ContextHandlerFunc(fn) +} diff --git a/bootcfg/api/handlers_test.go b/bootcfg/api/handlers_test.go index 7b55d7d2..5643cef9 100644 --- a/bootcfg/api/handlers_test.go +++ b/bootcfg/api/handlers_test.go @@ -7,6 +7,10 @@ import ( "testing" "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + + "github.com/coreos/coreos-baremetal/bootcfg/server" + "github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb" ) func TestRequireGET(t *testing.T) { @@ -32,3 +36,48 @@ func TestRequireGET_WrongMethod(t *testing.T) { assert.Equal(t, http.StatusMethodNotAllowed, w.Code) assert.Equal(t, "only HTTP GET is supported\n", w.Body.String()) } + +func TestSelectGroup(t *testing.T) { + store := &fixedStore{ + Groups: map[string]*storagepb.Group{testGroup.Id: testGroup}, + } + srv := server.NewServer(&server.Config{Store: 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) + fmt.Fprintf(w, "next handler called") + } + // assert that: + // - query params are used to match uuid=a1b2c3d4 to testGroup + // - the testGroup is added to the context + // - next handler is called + h := selectGroup(srv, ContextHandlerFunc(next)) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "?uuid=a1b2c3d4", nil) + h.ServeHTTP(context.Background(), w, req) + assert.Equal(t, "next handler called", w.Body.String()) +} + +func TestSelectProfile(t *testing.T) { + store := &fixedStore{ + Groups: map[string]*storagepb.Group{testGroup.Id: testGroup}, + Profiles: map[string]*storagepb.Profile{testGroup.Profile: testProfile}, + } + srv := server.NewServer(&server.Config{Store: store}) + next := func(ctx context.Context, w http.ResponseWriter, req *http.Request) { + profile, err := profileFromContext(ctx) + assert.Nil(t, err) + assert.Equal(t, testProfile, profile) + fmt.Fprintf(w, "next handler called") + } + // assert that: + // - query params are used to match uuid=a1b2c3d4 to testGroup's testProfile + // - the testProfile is added to the context + // - next handler is called + h := selectProfile(srv, ContextHandlerFunc(next)) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "?uuid=a1b2c3d4", nil) + h.ServeHTTP(context.Background(), w, req) + assert.Equal(t, "next handler called", w.Body.String()) +} diff --git a/bootcfg/api/http_test.go b/bootcfg/api/http_test.go index efb0f97d..c8d98d7c 100644 --- a/bootcfg/api/http_test.go +++ b/bootcfg/api/http_test.go @@ -10,6 +10,17 @@ import ( "golang.org/x/net/context" ) +func TestNewHandler(t *testing.T) { + fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "ContextHandler called") + } + h := NewHandler(ContextHandlerFunc(fn)) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/", nil) + h.ServeHTTP(w, req) + assert.Equal(t, "ContextHandler called", w.Body.String()) +} + func TestLabelsFromRequest(t *testing.T) { emptyMap := map[string]string{} cases := []struct { @@ -33,14 +44,3 @@ func TestLabelsFromRequest(t *testing.T) { assert.Equal(t, c.labels, labelsFromRequest(req)) } } - -func TestNewHandler(t *testing.T) { - fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) { - fmt.Fprintf(w, "ContextHandler called") - } - h := NewHandler(ContextHandlerFunc(fn)) - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - h.ServeHTTP(w, req) - assert.Equal(t, "ContextHandler called", w.Body.String()) -} diff --git a/bootcfg/api/pixiecore.go b/bootcfg/api/pixiecore.go index b4eae6b8..7f5e0e0e 100644 --- a/bootcfg/api/pixiecore.go +++ b/bootcfg/api/pixiecore.go @@ -4,14 +4,17 @@ import ( "net/http" "path/filepath" - "github.com/coreos/coreos-baremetal/bootcfg/storage" + "golang.org/x/net/context" + + "github.com/coreos/coreos-baremetal/bootcfg/server" + pb "github.com/coreos/coreos-baremetal/bootcfg/server/serverpb" ) // 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 storage.Store) http.Handler { - fn := func(w http.ResponseWriter, req *http.Request) { +func pixiecoreHandler(srv server.Server) ContextHandler { + fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) { macAddr, err := parseMAC(filepath.Base(req.URL.Path)) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -19,17 +22,17 @@ func pixiecoreHandler(gr *groupsResource, store storage.Store) http.Handler { } // pixiecore only provides MAC addresses attrs := map[string]string{"mac": macAddr.String()} - group, err := gr.findMatch(attrs) + group, err := srv.SelectGroup(ctx, &pb.SelectGroupRequest{Labels: attrs}) if err != nil { http.NotFound(w, req) return } - profile, err := store.ProfileGet(group.Profile) + resp, err := srv.ProfileGet(ctx, &pb.ProfileGetRequest{Id: group.Profile}) if err != nil { http.NotFound(w, req) return } - renderJSON(w, profile.Boot) + renderJSON(w, resp.Profile.Boot) } - return http.HandlerFunc(fn) + return ContextHandlerFunc(fn) } diff --git a/bootcfg/api/pixiecore_test.go b/bootcfg/api/pixiecore_test.go index 4e6f967f..2e3a614e 100644 --- a/bootcfg/api/pixiecore_test.go +++ b/bootcfg/api/pixiecore_test.go @@ -5,8 +5,11 @@ import ( "net/http/httptest" "testing" - "github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb" "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + + "github.com/coreos/coreos-baremetal/bootcfg/server" + "github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb" ) func TestPixiecoreHandler(t *testing.T) { @@ -14,12 +17,13 @@ func TestPixiecoreHandler(t *testing.T) { Groups: map[string]*storagepb.Group{testGroupWithMAC.Id: testGroupWithMAC}, Profiles: map[string]*storagepb.Profile{testGroupWithMAC.Profile: testProfile}, } - h := pixiecoreHandler(newGroupsResource(store), store) + srv := server.NewServer(&server.Config{Store: store}) + h := pixiecoreHandler(srv) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/"+validMACStr, nil) - h.ServeHTTP(w, req) + h.ServeHTTP(context.Background(), w, req) // assert that: - // - MAC address argument is used for Group matching + // - MAC address parameter 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) @@ -28,19 +32,21 @@ func TestPixiecoreHandler(t *testing.T) { } func TestPixiecoreHandler_InvalidMACAddress(t *testing.T) { - h := pixiecoreHandler(&groupsResource{}, &emptyStore{}) + srv := server.NewServer(&server.Config{Store: &emptyStore{}}) + h := pixiecoreHandler(srv) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) - h.ServeHTTP(w, req) + h.ServeHTTP(context.Background(), w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, "invalid MAC address /\n", w.Body.String()) } func TestPixiecoreHandler_NoMatchingGroup(t *testing.T) { - h := pixiecoreHandler(newGroupsResource(&emptyStore{}), &emptyStore{}) + srv := server.NewServer(&server.Config{Store: &emptyStore{}}) + h := pixiecoreHandler(srv) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/"+validMACStr, nil) - h.ServeHTTP(w, req) + h.ServeHTTP(context.Background(), w, req) assert.Equal(t, http.StatusNotFound, w.Code) } @@ -48,9 +54,10 @@ func TestPixiecoreHandler_NoMatchingProfile(t *testing.T) { store := &fixedStore{ Groups: map[string]*storagepb.Group{testGroup.Id: testGroup}, } - h := pixiecoreHandler(newGroupsResource(store), &emptyStore{}) + srv := server.NewServer(&server.Config{Store: store}) + h := pixiecoreHandler(srv) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/"+validMACStr, nil) - h.ServeHTTP(w, req) + h.ServeHTTP(context.Background(), w, req) assert.Equal(t, http.StatusNotFound, w.Code) } diff --git a/bootcfg/api/server.go b/bootcfg/api/server.go index 6013b976..b7eb4496 100644 --- a/bootcfg/api/server.go +++ b/bootcfg/api/server.go @@ -3,14 +3,11 @@ package api import ( "net/http" + "github.com/coreos/pkg/capnslog" + + "github.com/coreos/coreos-baremetal/bootcfg/server" "github.com/coreos/coreos-baremetal/bootcfg/sign" "github.com/coreos/coreos-baremetal/bootcfg/storage" - "github.com/coreos/pkg/capnslog" -) - -const ( - // APIVersion of the api server and its config types. - APIVersion = "v1alpha1" ) var log = capnslog.NewPackageLogger("github.com/coreos/coreos-baremetal/bootcfg", "api") @@ -26,7 +23,7 @@ type Config struct { ArmoredSigner sign.Signer } -// Server serves boot and provisioning configs to machines. +// Server serves boot and provisioning configs to machines via HTTP. type Server struct { store storage.Store assetsPath string @@ -47,49 +44,49 @@ func NewServer(config *Config) *Server { // HTTPHandler returns a HTTP handler for the server. func (s *Server) HTTPHandler() http.Handler { mux := http.NewServeMux() - gr := newGroupsResource(s.store) + srv := server.NewServer(&server.Config{s.store}) // Boot via GRUB - mux.Handle("/grub", logRequests(NewHandler(gr.matchProfileHandler(grubHandler())))) + mux.Handle("/grub", logRequests(NewHandler(selectProfile(srv, grubHandler())))) // Boot via iPXE mux.Handle("/boot.ipxe", logRequests(ipxeInspect())) mux.Handle("/boot.ipxe.0", logRequests(ipxeInspect())) - mux.Handle("/ipxe", logRequests(NewHandler(gr.matchProfileHandler(ipxeHandler())))) + mux.Handle("/ipxe", logRequests(NewHandler(selectProfile(srv, ipxeHandler())))) // Boot via Pixiecore - mux.Handle("/pixiecore/v1/boot/", logRequests(pixiecoreHandler(gr, s.store))) + mux.Handle("/pixiecore/v1/boot/", logRequests(NewHandler(pixiecoreHandler(srv)))) // Ignition Config - mux.Handle("/ignition", logRequests(NewHandler(gr.matchGroupHandler(ignitionHandler(s.store))))) + mux.Handle("/ignition", logRequests(NewHandler(selectGroup(srv, ignitionHandler(s.store))))) // Cloud-Config - mux.Handle("/cloud", logRequests(NewHandler(gr.matchGroupHandler(cloudHandler(s.store))))) + mux.Handle("/cloud", logRequests(NewHandler(selectGroup(srv, cloudHandler(s.store))))) // metadata - mux.Handle("/metadata", logRequests(NewHandler(gr.matchGroupHandler(metadataHandler())))) + mux.Handle("/metadata", logRequests(NewHandler(selectGroup(srv, metadataHandler())))) // Signatures if s.signer != nil { signerChain := func(next http.Handler) http.Handler { return logRequests(sign.SignatureHandler(s.signer, next)) } - mux.Handle("/grub.sig", signerChain(NewHandler(gr.matchProfileHandler(grubHandler())))) + mux.Handle("/grub.sig", signerChain(NewHandler(selectProfile(srv, grubHandler())))) mux.Handle("/boot.ipxe.sig", signerChain(ipxeInspect())) mux.Handle("/boot.ipxe.0.sig", signerChain(ipxeInspect())) - mux.Handle("/ipxe.sig", signerChain(NewHandler(gr.matchProfileHandler(ipxeHandler())))) - mux.Handle("/pixiecore/v1/boot.sig/", signerChain(pixiecoreHandler(gr, 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())))) + mux.Handle("/ipxe.sig", signerChain(NewHandler(selectProfile(srv, ipxeHandler())))) + mux.Handle("/pixiecore/v1/boot.sig/", signerChain(NewHandler(pixiecoreHandler(srv)))) + mux.Handle("/ignition.sig", signerChain(NewHandler(selectGroup(srv, ignitionHandler(s.store))))) + mux.Handle("/cloud.sig", signerChain(NewHandler(selectGroup(srv, cloudHandler(s.store))))) + mux.Handle("/metadata.sig", signerChain(NewHandler(selectGroup(srv, 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.matchProfileHandler(grubHandler())))) + mux.Handle("/grub.asc", signerChain(NewHandler(selectProfile(srv, grubHandler())))) mux.Handle("/boot.ipxe.asc", signerChain(ipxeInspect())) mux.Handle("/boot.ipxe.0.asc", signerChain(ipxeInspect())) - mux.Handle("/ipxe.asc", signerChain(NewHandler(gr.matchProfileHandler(ipxeHandler())))) - mux.Handle("/pixiecore/v1/boot.asc/", signerChain(pixiecoreHandler(gr, 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())))) + mux.Handle("/ipxe.asc", signerChain(NewHandler(selectProfile(srv, ipxeHandler())))) + mux.Handle("/pixiecore/v1/boot.asc/", signerChain(NewHandler(pixiecoreHandler(srv)))) + mux.Handle("/ignition.asc", signerChain(NewHandler(selectGroup(srv, ignitionHandler(s.store))))) + mux.Handle("/cloud.asc", signerChain(NewHandler(selectGroup(srv, cloudHandler(s.store))))) + mux.Handle("/metadata.asc", signerChain(NewHandler(selectGroup(srv, metadataHandler())))) } // kernel, initrd, and TLS assets diff --git a/bootcfg/rpc/grpc.go b/bootcfg/rpc/grpc.go index cce91f5f..847dc664 100644 --- a/bootcfg/rpc/grpc.go +++ b/bootcfg/rpc/grpc.go @@ -12,5 +12,6 @@ func NewServer(s server.Server, opts ...grpc.ServerOption) (*grpc.Server, error) grpcServer := grpc.NewServer(opts...) pb.RegisterGroupsServer(grpcServer, s) pb.RegisterProfilesServer(grpcServer, s) + pb.RegisterSelectServer(grpcServer, newSelectServer(s)) return grpcServer, nil } diff --git a/bootcfg/rpc/select.go b/bootcfg/rpc/select.go new file mode 100644 index 00000000..449882cf --- /dev/null +++ b/bootcfg/rpc/select.go @@ -0,0 +1,30 @@ +package rpc + +import ( + "golang.org/x/net/context" + + "github.com/coreos/coreos-baremetal/bootcfg/server" + pb "github.com/coreos/coreos-baremetal/bootcfg/server/serverpb" +) + +// selectServer wraps a bootcfg Server to be suitable for gRPC registration. +type selectServer struct { + // bootcfg Server + srv server.Server +} + +func newSelectServer(s server.Server) pb.SelectServer { + return &selectServer{ + srv: s, + } +} + +func (s *selectServer) SelectGroup(ctx context.Context, req *pb.SelectGroupRequest) (*pb.SelectGroupResponse, error) { + group, err := s.srv.SelectGroup(ctx, req) + return &pb.SelectGroupResponse{Group: group}, err +} + +func (s *selectServer) SelectProfile(ctx context.Context, req *pb.SelectProfileRequest) (*pb.SelectProfileResponse, error) { + profile, err := s.srv.SelectProfile(ctx, req) + return &pb.SelectProfileResponse{Profile: profile}, err +} diff --git a/bootcfg/server/server.go b/bootcfg/server/server.go index 50caa277..87c81087 100644 --- a/bootcfg/server/server.go +++ b/bootcfg/server/server.go @@ -1,16 +1,27 @@ package server import ( + "errors" + "sort" + "golang.org/x/net/context" pb "github.com/coreos/coreos-baremetal/bootcfg/server/serverpb" "github.com/coreos/coreos-baremetal/bootcfg/storage" + "github.com/coreos/coreos-baremetal/bootcfg/storage/storagepb" +) + +var ( + errNoMatchingGroup = errors.New("bootcfg: No matching Group") + errNoProfileFound = errors.New("bootcfg: No Profile found") ) // Server defines a bootcfg Server. type Server interface { pb.GroupsServer pb.ProfilesServer + SelectGroup(ctx context.Context, req *pb.SelectGroupRequest) (*storagepb.Group, error) + SelectProfile(ctx context.Context, req *pb.SelectProfileRequest) (*storagepb.Profile, error) } // Config configures a server implementation. @@ -72,3 +83,33 @@ func (s *server) ProfileList(ctx context.Context, req *pb.ProfileListRequest) (* } return &pb.ProfileListResponse{Profiles: profiles}, nil } + +// SelectGroup selects the Group whose selector matches the given labels. +// Groups are evaluated in sorted order from most selectors to least, using +// alphabetical order as a deterministic tie-breaker. +func (s *server) SelectGroup(ctx context.Context, req *pb.SelectGroupRequest) (*storagepb.Group, error) { + groups, err := s.store.GroupList() + if err != nil { + return nil, err + } + sort.Sort(sort.Reverse(storagepb.ByReqs(groups))) + for _, group := range groups { + if group.Matches(req.Labels) { + return group, nil + } + } + return nil, errNoMatchingGroup +} + +func (s *server) SelectProfile(ctx context.Context, req *pb.SelectProfileRequest) (*storagepb.Profile, error) { + group, err := s.SelectGroup(ctx, &pb.SelectGroupRequest{Labels: req.Labels}) + if err == nil { + // lookup the Profile by id + resp, err := s.ProfileGet(ctx, &pb.ProfileGetRequest{Id: group.Profile}) + if err == nil { + return resp.Profile, nil + } + return nil, errNoProfileFound + } + return nil, errNoMatchingGroup +} diff --git a/bootcfg/server/serverpb/rpc.pb.go b/bootcfg/server/serverpb/rpc.pb.go index 14370bb1..f55cfae6 100644 --- a/bootcfg/server/serverpb/rpc.pb.go +++ b/bootcfg/server/serverpb/rpc.pb.go @@ -9,6 +9,10 @@ It is generated from these files: rpc.proto It has these top-level messages: + SelectGroupRequest + SelectGroupResponse + SelectProfileRequest + SelectProfileResponse GroupGetRequest GroupListRequest GroupGetResponse @@ -41,6 +45,70 @@ var _ = math.Inf // is compatible with the proto package it is being compiled against. const _ = proto.ProtoPackageIsVersion1 +type SelectGroupRequest struct { + Labels map[string]string `protobuf:"bytes,1,rep,name=labels" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +} + +func (m *SelectGroupRequest) Reset() { *m = SelectGroupRequest{} } +func (m *SelectGroupRequest) String() string { return proto.CompactTextString(m) } +func (*SelectGroupRequest) ProtoMessage() {} +func (*SelectGroupRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *SelectGroupRequest) GetLabels() map[string]string { + if m != nil { + return m.Labels + } + return nil +} + +type SelectGroupResponse struct { + Group *storagepb.Group `protobuf:"bytes,1,opt,name=group" json:"group,omitempty"` +} + +func (m *SelectGroupResponse) Reset() { *m = SelectGroupResponse{} } +func (m *SelectGroupResponse) String() string { return proto.CompactTextString(m) } +func (*SelectGroupResponse) ProtoMessage() {} +func (*SelectGroupResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func (m *SelectGroupResponse) GetGroup() *storagepb.Group { + if m != nil { + return m.Group + } + return nil +} + +type SelectProfileRequest struct { + Labels map[string]string `protobuf:"bytes,1,rep,name=labels" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +} + +func (m *SelectProfileRequest) Reset() { *m = SelectProfileRequest{} } +func (m *SelectProfileRequest) String() string { return proto.CompactTextString(m) } +func (*SelectProfileRequest) ProtoMessage() {} +func (*SelectProfileRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } + +func (m *SelectProfileRequest) GetLabels() map[string]string { + if m != nil { + return m.Labels + } + return nil +} + +type SelectProfileResponse struct { + Profile *storagepb.Profile `protobuf:"bytes,1,opt,name=profile" json:"profile,omitempty"` +} + +func (m *SelectProfileResponse) Reset() { *m = SelectProfileResponse{} } +func (m *SelectProfileResponse) String() string { return proto.CompactTextString(m) } +func (*SelectProfileResponse) ProtoMessage() {} +func (*SelectProfileResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } + +func (m *SelectProfileResponse) GetProfile() *storagepb.Profile { + if m != nil { + return m.Profile + } + return nil +} + type GroupGetRequest struct { Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` } @@ -48,7 +116,7 @@ type GroupGetRequest struct { func (m *GroupGetRequest) Reset() { *m = GroupGetRequest{} } func (m *GroupGetRequest) String() string { return proto.CompactTextString(m) } func (*GroupGetRequest) ProtoMessage() {} -func (*GroupGetRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } +func (*GroupGetRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } type GroupListRequest struct { } @@ -56,7 +124,7 @@ type GroupListRequest struct { func (m *GroupListRequest) Reset() { *m = GroupListRequest{} } func (m *GroupListRequest) String() string { return proto.CompactTextString(m) } func (*GroupListRequest) ProtoMessage() {} -func (*GroupListRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } +func (*GroupListRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } type GroupGetResponse struct { Group *storagepb.Group `protobuf:"bytes,1,opt,name=group" json:"group,omitempty"` @@ -65,7 +133,7 @@ type GroupGetResponse struct { func (m *GroupGetResponse) Reset() { *m = GroupGetResponse{} } func (m *GroupGetResponse) String() string { return proto.CompactTextString(m) } func (*GroupGetResponse) ProtoMessage() {} -func (*GroupGetResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } +func (*GroupGetResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } func (m *GroupGetResponse) GetGroup() *storagepb.Group { if m != nil { @@ -81,7 +149,7 @@ type GroupListResponse struct { func (m *GroupListResponse) Reset() { *m = GroupListResponse{} } func (m *GroupListResponse) String() string { return proto.CompactTextString(m) } func (*GroupListResponse) ProtoMessage() {} -func (*GroupListResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } +func (*GroupListResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } func (m *GroupListResponse) GetGroups() []*storagepb.Group { if m != nil { @@ -97,7 +165,7 @@ type ProfilePutRequest struct { func (m *ProfilePutRequest) Reset() { *m = ProfilePutRequest{} } func (m *ProfilePutRequest) String() string { return proto.CompactTextString(m) } func (*ProfilePutRequest) ProtoMessage() {} -func (*ProfilePutRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } +func (*ProfilePutRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} } func (m *ProfilePutRequest) GetProfile() *storagepb.Profile { if m != nil { @@ -112,7 +180,7 @@ type ProfilePutResponse struct { func (m *ProfilePutResponse) Reset() { *m = ProfilePutResponse{} } func (m *ProfilePutResponse) String() string { return proto.CompactTextString(m) } func (*ProfilePutResponse) ProtoMessage() {} -func (*ProfilePutResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } +func (*ProfilePutResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} } type ProfileGetRequest struct { Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` @@ -121,7 +189,7 @@ type ProfileGetRequest struct { func (m *ProfileGetRequest) Reset() { *m = ProfileGetRequest{} } func (m *ProfileGetRequest) String() string { return proto.CompactTextString(m) } func (*ProfileGetRequest) ProtoMessage() {} -func (*ProfileGetRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } +func (*ProfileGetRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} } type ProfileGetResponse struct { Profile *storagepb.Profile `protobuf:"bytes,1,opt,name=profile" json:"profile,omitempty"` @@ -130,7 +198,7 @@ type ProfileGetResponse struct { func (m *ProfileGetResponse) Reset() { *m = ProfileGetResponse{} } func (m *ProfileGetResponse) String() string { return proto.CompactTextString(m) } func (*ProfileGetResponse) ProtoMessage() {} -func (*ProfileGetResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } +func (*ProfileGetResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11} } func (m *ProfileGetResponse) GetProfile() *storagepb.Profile { if m != nil { @@ -145,7 +213,7 @@ type ProfileListRequest struct { func (m *ProfileListRequest) Reset() { *m = ProfileListRequest{} } func (m *ProfileListRequest) String() string { return proto.CompactTextString(m) } func (*ProfileListRequest) ProtoMessage() {} -func (*ProfileListRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} } +func (*ProfileListRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} } type ProfileListResponse struct { Profiles []*storagepb.Profile `protobuf:"bytes,1,rep,name=profiles" json:"profiles,omitempty"` @@ -154,7 +222,7 @@ type ProfileListResponse struct { func (m *ProfileListResponse) Reset() { *m = ProfileListResponse{} } func (m *ProfileListResponse) String() string { return proto.CompactTextString(m) } func (*ProfileListResponse) ProtoMessage() {} -func (*ProfileListResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} } +func (*ProfileListResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{13} } func (m *ProfileListResponse) GetProfiles() []*storagepb.Profile { if m != nil { @@ -164,6 +232,10 @@ func (m *ProfileListResponse) GetProfiles() []*storagepb.Profile { } func init() { + proto.RegisterType((*SelectGroupRequest)(nil), "serverpb.SelectGroupRequest") + proto.RegisterType((*SelectGroupResponse)(nil), "serverpb.SelectGroupResponse") + proto.RegisterType((*SelectProfileRequest)(nil), "serverpb.SelectProfileRequest") + proto.RegisterType((*SelectProfileResponse)(nil), "serverpb.SelectProfileResponse") proto.RegisterType((*GroupGetRequest)(nil), "serverpb.GroupGetRequest") proto.RegisterType((*GroupListRequest)(nil), "serverpb.GroupListRequest") proto.RegisterType((*GroupGetResponse)(nil), "serverpb.GroupGetResponse") @@ -389,29 +461,127 @@ var _Profiles_serviceDesc = grpc.ServiceDesc{ Streams: []grpc.StreamDesc{}, } -var fileDescriptor0 = []byte{ - // 368 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x53, 0x4d, 0x4e, 0xeb, 0x30, - 0x10, 0x7e, 0x79, 0x88, 0x92, 0x4e, 0x25, 0x68, 0x0d, 0x0b, 0x08, 0x45, 0x02, 0x23, 0xa1, 0x2e, - 0xc0, 0x95, 0xca, 0x0e, 0x89, 0x05, 0x20, 0xa8, 0x40, 0x5d, 0x54, 0xb9, 0x41, 0x13, 0xdc, 0x10, - 0xa9, 0xc5, 0xc6, 0x76, 0xb8, 0x09, 0xd7, 0xe4, 0x0c, 0xb4, 0x8e, 0xed, 0x9a, 0x26, 0xb0, 0x60, - 0x65, 0x6b, 0xbe, 0x1f, 0xcf, 0xcc, 0x97, 0x40, 0x53, 0xf0, 0x94, 0x70, 0xc1, 0x14, 0x43, 0xa1, - 0xa4, 0xe2, 0x9d, 0x0a, 0x9e, 0x44, 0x4f, 0x59, 0xae, 0x5e, 0x8a, 0x84, 0xa4, 0x6c, 0xde, 0x4f, - 0x99, 0xa0, 0x4c, 0x9a, 0xe3, 0x22, 0x99, 0x08, 0x3a, 0xa7, 0x6a, 0x32, 0xeb, 0x27, 0x8c, 0xa9, - 0x74, 0x9a, 0xf5, 0xa5, 0x62, 0x62, 0x92, 0x51, 0x7b, 0xf2, 0xc4, 0xde, 0x4a, 0x57, 0x7c, 0x02, - 0x3b, 0x43, 0xc1, 0x0a, 0x3e, 0xa4, 0x2a, 0xa6, 0x6f, 0x05, 0x95, 0x0a, 0x6d, 0xc3, 0xff, 0xfc, - 0x79, 0x3f, 0x38, 0x0e, 0x7a, 0xcd, 0x78, 0x71, 0xc3, 0x08, 0xda, 0x9a, 0x32, 0xca, 0xa5, 0xe5, - 0xe0, 0x2b, 0x53, 0xd3, 0x32, 0xc9, 0xd9, 0xab, 0xa4, 0xe8, 0x0c, 0x36, 0xb3, 0x65, 0x4d, 0x4b, - 0x5b, 0x83, 0x36, 0x71, 0x6f, 0x12, 0xcd, 0x8d, 0x4b, 0x18, 0x5f, 0x43, 0xc7, 0xf3, 0x33, 0xe2, - 0x1e, 0x34, 0x34, 0x2a, 0x17, 0xea, 0x8d, 0x5a, 0xb5, 0xc1, 0xf1, 0x0d, 0x74, 0xc6, 0x82, 0x4d, - 0xf3, 0x19, 0x1d, 0x17, 0xae, 0xe7, 0x73, 0xd8, 0xe2, 0x65, 0xd1, 0xbc, 0x8e, 0x3c, 0xbd, 0xa1, - 0xc7, 0x96, 0x82, 0xf7, 0x00, 0xf9, 0x16, 0x65, 0x0b, 0xf8, 0xd4, 0x19, 0xff, 0xb2, 0x8c, 0x5b, - 0x27, 0xf5, 0x47, 0xff, 0xeb, 0xf3, 0xfe, 0x4a, 0xef, 0x61, 0xf7, 0x5b, 0xd5, 0x58, 0x13, 0x08, - 0x8d, 0xce, 0xae, 0xa6, 0xce, 0xdb, 0x71, 0x06, 0x1f, 0x01, 0x34, 0xf4, 0xc2, 0x24, 0xba, 0x83, - 0xd0, 0x86, 0x84, 0x0e, 0x88, 0xfd, 0x7c, 0xc8, 0x5a, 0xde, 0x51, 0x54, 0x07, 0x99, 0x9d, 0xfc, - 0x43, 0x0f, 0xd0, 0x74, 0x69, 0xa1, 0x75, 0xaa, 0xd7, 0x7f, 0x74, 0x58, 0x8b, 0x59, 0x9f, 0xc1, - 0x67, 0x00, 0xa1, 0xe9, 0x56, 0xa2, 0x47, 0x80, 0x55, 0x00, 0xc8, 0x53, 0x56, 0x92, 0x8d, 0xba, - 0xf5, 0xa0, 0xeb, 0x6f, 0x65, 0xb5, 0x1c, 0xb3, 0x6a, 0xe5, 0x0d, 0xda, 0xad, 0x07, 0x9d, 0xd5, - 0x08, 0x5a, 0x5e, 0x02, 0xa8, 0x4a, 0xf7, 0xc7, 0x3d, 0xfa, 0x01, 0xb5, 0x6e, 0x49, 0x43, 0xff, - 0x60, 0x97, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xf4, 0x2a, 0x65, 0x01, 0xc3, 0x03, 0x00, 0x00, +// Client API for Select service + +type SelectClient interface { + // SelectGroup returns the Group matching the given labels. + SelectGroup(ctx context.Context, in *SelectGroupRequest, opts ...grpc.CallOption) (*SelectGroupResponse, error) + // SelectProfile returns the Profile matching the given labels. + SelectProfile(ctx context.Context, in *SelectProfileRequest, opts ...grpc.CallOption) (*SelectProfileResponse, error) +} + +type selectClient struct { + cc *grpc.ClientConn +} + +func NewSelectClient(cc *grpc.ClientConn) SelectClient { + return &selectClient{cc} +} + +func (c *selectClient) SelectGroup(ctx context.Context, in *SelectGroupRequest, opts ...grpc.CallOption) (*SelectGroupResponse, error) { + out := new(SelectGroupResponse) + err := grpc.Invoke(ctx, "/serverpb.Select/SelectGroup", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *selectClient) SelectProfile(ctx context.Context, in *SelectProfileRequest, opts ...grpc.CallOption) (*SelectProfileResponse, error) { + out := new(SelectProfileResponse) + err := grpc.Invoke(ctx, "/serverpb.Select/SelectProfile", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for Select service + +type SelectServer interface { + // SelectGroup returns the Group matching the given labels. + SelectGroup(context.Context, *SelectGroupRequest) (*SelectGroupResponse, error) + // SelectProfile returns the Profile matching the given labels. + SelectProfile(context.Context, *SelectProfileRequest) (*SelectProfileResponse, error) +} + +func RegisterSelectServer(s *grpc.Server, srv SelectServer) { + s.RegisterService(&_Select_serviceDesc, srv) +} + +func _Select_SelectGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(SelectGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(SelectServer).SelectGroup(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _Select_SelectProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(SelectProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(SelectServer).SelectProfile(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +var _Select_serviceDesc = grpc.ServiceDesc{ + ServiceName: "serverpb.Select", + HandlerType: (*SelectServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SelectGroup", + Handler: _Select_SelectGroup_Handler, + }, + { + MethodName: "SelectProfile", + Handler: _Select_SelectProfile_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, +} + +var fileDescriptor0 = []byte{ + // 520 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xac, 0x54, 0x5d, 0x6e, 0xd3, 0x40, + 0x10, 0xc6, 0xa9, 0x1a, 0x92, 0x89, 0x80, 0x74, 0x1a, 0xa4, 0x62, 0xca, 0x9f, 0x91, 0x50, 0x84, + 0xc0, 0x91, 0xc2, 0x0b, 0x54, 0xaa, 0x04, 0x45, 0xa1, 0x02, 0xe5, 0xa1, 0x0a, 0x27, 0xb0, 0xcd, + 0x34, 0x44, 0xb8, 0x5d, 0xb3, 0xbb, 0xae, 0xd4, 0x63, 0xf0, 0xc0, 0x25, 0x38, 0x1c, 0x67, 0xc0, + 0x59, 0xef, 0x6e, 0x36, 0xb6, 0x5b, 0x29, 0xa8, 0x4f, 0x5e, 0xcf, 0xf7, 0xcd, 0xb7, 0xf3, 0xcd, + 0xd8, 0x03, 0x5d, 0x9e, 0x25, 0x61, 0xc6, 0x99, 0x64, 0xd8, 0x11, 0xc4, 0x2f, 0x88, 0x67, 0xb1, + 0xff, 0x65, 0xbe, 0x90, 0xdf, 0xf3, 0x38, 0x4c, 0xd8, 0xd9, 0x28, 0x61, 0x9c, 0x98, 0xd0, 0x8f, + 0xd7, 0x71, 0xc4, 0xe9, 0x8c, 0x64, 0x94, 0x8e, 0x62, 0xc6, 0x64, 0x72, 0x3a, 0x1f, 0x09, 0xc9, + 0x78, 0x34, 0x27, 0xf3, 0xcc, 0x62, 0x73, 0x2a, 0x55, 0x83, 0x5f, 0x1e, 0xe0, 0x57, 0x4a, 0x29, + 0x91, 0xc7, 0x9c, 0xe5, 0xd9, 0x8c, 0x7e, 0xe6, 0x24, 0x24, 0xbe, 0x87, 0x76, 0x1a, 0xc5, 0x94, + 0x8a, 0x3d, 0xef, 0xe9, 0xd6, 0xb0, 0x37, 0x1e, 0x86, 0xe6, 0xf6, 0xb0, 0xce, 0x0e, 0xa7, 0x8a, + 0x3a, 0x39, 0x97, 0xfc, 0x72, 0xa6, 0xf3, 0xfc, 0x77, 0xd0, 0x73, 0xc2, 0xd8, 0x87, 0xad, 0x1f, + 0x74, 0x59, 0xa8, 0x79, 0xc3, 0xee, 0x6c, 0x79, 0xc4, 0x01, 0x6c, 0x5f, 0x44, 0x69, 0x4e, 0x7b, + 0x2d, 0x15, 0x2b, 0x5f, 0x0e, 0x5a, 0x6f, 0xbd, 0xe0, 0x10, 0x76, 0xd7, 0x2e, 0x11, 0x19, 0x3b, + 0x17, 0x84, 0x2f, 0x60, 0x7b, 0xbe, 0x0c, 0x28, 0x91, 0xde, 0xb8, 0x1f, 0x5a, 0x4f, 0x61, 0x49, + 0x2c, 0xe1, 0xe0, 0xb7, 0x07, 0x83, 0x32, 0xff, 0x84, 0xb3, 0xd3, 0x45, 0x4a, 0xc6, 0xd4, 0x51, + 0xc5, 0xd4, 0xcb, 0xaa, 0xa9, 0x75, 0xfe, 0x4d, 0xdb, 0x9a, 0xc0, 0xfd, 0xca, 0x35, 0xda, 0xd8, + 0x2b, 0xb8, 0x9d, 0x95, 0x21, 0x6d, 0x0d, 0x1d, 0x6b, 0x86, 0x6c, 0x28, 0xc1, 0x33, 0xb8, 0xa7, + 0xec, 0x1e, 0x93, 0x34, 0xc6, 0xee, 0x42, 0x6b, 0xf1, 0x4d, 0x17, 0x51, 0x9c, 0x02, 0x84, 0xbe, + 0xa2, 0x4c, 0x17, 0xc2, 0x70, 0x82, 0x03, 0x1d, 0x53, 0x69, 0x1b, 0x76, 0xf4, 0x10, 0x76, 0x1c, + 0x3d, 0x9d, 0x3c, 0x84, 0xb6, 0x42, 0x4d, 0x37, 0xeb, 0xd9, 0x1a, 0x0f, 0x3e, 0xc0, 0x8e, 0x76, + 0x71, 0x92, 0xdb, 0x9a, 0x37, 0x33, 0x3d, 0x00, 0x74, 0x25, 0xca, 0x12, 0x82, 0xe7, 0x56, 0xf8, + 0x9a, 0x66, 0x1c, 0xd9, 0x54, 0xd7, 0xfa, 0xff, 0x5e, 0xef, 0xb6, 0x74, 0x02, 0xbb, 0x6b, 0x51, + 0x2d, 0x1d, 0x42, 0x47, 0xe7, 0x99, 0xd6, 0x34, 0x69, 0x5b, 0xce, 0xb8, 0xf8, 0x5e, 0xdb, 0xaa, + 0x61, 0x02, 0x3f, 0x42, 0xc7, 0x0c, 0x09, 0x1f, 0xac, 0xbe, 0xce, 0xca, 0xbc, 0x7d, 0xbf, 0x09, + 0xd2, 0x3d, 0xb9, 0x85, 0x9f, 0xa0, 0x6b, 0xa7, 0x85, 0x55, 0xaa, 0x53, 0xbf, 0xff, 0xb0, 0x11, + 0x33, 0x3a, 0xe3, 0xbf, 0x1e, 0x74, 0x74, 0xb5, 0x02, 0x3f, 0x03, 0xac, 0x06, 0x80, 0x4e, 0x66, + 0x6d, 0xb2, 0xfe, 0x7e, 0x33, 0x68, 0xeb, 0x5b, 0x49, 0x2d, 0x6d, 0xd6, 0xa5, 0x1c, 0xa3, 0xfb, + 0xcd, 0xa0, 0x95, 0x9a, 0x42, 0xcf, 0x99, 0x00, 0xd6, 0xe9, 0xae, 0xdd, 0x47, 0x57, 0xa0, 0xd6, + 0xf0, 0x9f, 0x62, 0x10, 0xe5, 0x1f, 0xba, 0x14, 0x76, 0x56, 0x90, 0x2b, 0x5c, 0x5f, 0x7f, 0xae, + 0x70, 0xc3, 0xde, 0x2a, 0xca, 0x9c, 0xc1, 0x9d, 0xb5, 0x3f, 0x1f, 0x1f, 0x5f, 0xbf, 0x79, 0xfc, + 0x27, 0x57, 0xe2, 0x46, 0x33, 0x6e, 0xab, 0xfd, 0xfd, 0xe6, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, + 0x30, 0x49, 0x0e, 0x1d, 0x22, 0x06, 0x00, 0x00, } diff --git a/bootcfg/server/serverpb/rpc.proto b/bootcfg/server/serverpb/rpc.proto index f11e6a5f..8305459e 100644 --- a/bootcfg/server/serverpb/rpc.proto +++ b/bootcfg/server/serverpb/rpc.proto @@ -19,6 +19,29 @@ service Profiles { rpc ProfileList(ProfileListRequest) returns (ProfileListResponse) {}; } +service Select { + // SelectGroup returns the Group matching the given labels. + rpc SelectGroup(SelectGroupRequest) returns (SelectGroupResponse) {}; + // SelectProfile returns the Profile matching the given labels. + rpc SelectProfile(SelectProfileRequest) returns (SelectProfileResponse) {}; +} + +message SelectGroupRequest { + map labels = 1; +} + +message SelectGroupResponse { + storagepb.Group group = 1; +} + +message SelectProfileRequest { + map labels = 1; +} + +message SelectProfileResponse { + storagepb.Profile profile = 1; +} + message GroupGetRequest { string id = 1; } @@ -52,7 +75,3 @@ message ProfileListRequest {} message ProfileListResponse { repeated storagepb.Profile profiles = 1; } - - - - diff --git a/bootcfg/storage/storagepb/group.go b/bootcfg/storage/storagepb/group.go index 0d30dd01..85f49c77 100644 --- a/bootcfg/storage/storagepb/group.go +++ b/bootcfg/storage/storagepb/group.go @@ -28,7 +28,7 @@ func (g *Group) requirementString() string { return strings.Join(reqs, ",") } -// byReqs defines a collection of Group structs which have a deterministic +// ByReqs defines a collection of Group structs which have a deterministic // 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}.