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
This commit is contained in:
Dalton Hubble
2016-03-28 11:01:20 -07:00
parent beb1985b22
commit e0bfb3947b
14 changed files with 450 additions and 261 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

30
bootcfg/rpc/select.go Normal file
View File

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

View File

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

View File

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

View File

@@ -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<string, string> labels = 1;
}
message SelectGroupResponse {
storagepb.Group group = 1;
}
message SelectProfileRequest {
map<string, string> 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;
}

View File

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