From 5f03df7133e400b25a7760b3ce6858e8850ddd0d Mon Sep 17 00:00:00 2001 From: Jonathan Boulle Date: Tue, 26 Aug 2014 21:58:25 -0700 Subject: [PATCH] API: support query filters for States endpoint --- api/state.go | 26 +++++++++++-- api/state_test.go | 95 +++++++++++++++++++++++++++++++++++++++++++++++ registry/fake.go | 38 ++++++++++++------- 3 files changed, 143 insertions(+), 16 deletions(-) diff --git a/api/state.go b/api/state.go index 15231948e..ff345b96e 100644 --- a/api/state.go +++ b/api/state.go @@ -42,7 +42,17 @@ func (sr *stateResource) list(rw http.ResponseWriter, req *http.Request) { token = &def } - page, err := getUnitStatePage(sr.cAPI, *token) + var machineID, unitName string + for _, val := range req.URL.Query()["machineID"] { + machineID = val + break + } + for _, val := range req.URL.Query()["unitName"] { + unitName = val + break + } + + page, err := getUnitStatePage(sr.cAPI, machineID, unitName, *token) if err != nil { log.Errorf("Failed fetching page of UnitStates: %v", err) sendError(rw, http.StatusInternalServerError, nil) @@ -52,13 +62,23 @@ func (sr *stateResource) list(rw http.ResponseWriter, req *http.Request) { sendResponse(rw, http.StatusOK, &page) } -func getUnitStatePage(cAPI client.API, tok PageToken) (*schema.UnitStatePage, error) { +func getUnitStatePage(cAPI client.API, machineID, unitName string, tok PageToken) (*schema.UnitStatePage, error) { states, err := cAPI.UnitStates() if err != nil { return nil, err } + var filtered []*schema.UnitState + for _, us := range states { + if machineID != "" && machineID != us.MachineID { + continue + } + if unitName != "" && unitName != us.Name { + continue + } + filtered = append(filtered, us) + } - items, next := extractUnitStatePageData(states, tok) + items, next := extractUnitStatePageData(filtered, tok) page := schema.UnitStatePage{ States: items, } diff --git a/api/state_test.go b/api/state_test.go index e887d58ba..047af8271 100644 --- a/api/state_test.go +++ b/api/state_test.go @@ -15,6 +15,101 @@ import ( ) func TestUnitStateList(t *testing.T) { + us1 := unit.UnitState{UnitName: "AAA", ActiveState: "active"} + us2 := unit.UnitState{UnitName: "BBB", ActiveState: "inactive", MachineID: "XXX"} + us3 := unit.UnitState{UnitName: "CCC", ActiveState: "active", MachineID: "XXX"} + us4 := unit.UnitState{UnitName: "CCC", ActiveState: "inactive", MachineID: "YYY"} + sus1 := &schema.UnitState{Name: "AAA", SystemdActiveState: "active"} + sus2 := &schema.UnitState{Name: "BBB", SystemdActiveState: "inactive", MachineID: "XXX"} + sus3 := &schema.UnitState{Name: "CCC", SystemdActiveState: "active", MachineID: "XXX"} + sus4 := &schema.UnitState{Name: "CCC", SystemdActiveState: "inactive", MachineID: "YYY"} + + for i, tt := range []struct { + url string + expected []*schema.UnitState + }{ + { + // Standard query - return all results + "http://example.com/state", + []*schema.UnitState{sus1, sus2, sus3, sus4}, + }, + { + // Query for specific unit name should be fine + "http://example.com/state?unitName=AAA", + []*schema.UnitState{sus1}, + }, + { + // Query for a different specific unit name should be fine + "http://example.com/state?unitName=CCC", + []*schema.UnitState{sus3, sus4}, + }, + { + // Query for nonexistent unit name should return nothing + "http://example.com/state?unitName=nope", + nil, + }, + { + // Query for a specific machine ID should be fine + "http://example.com/state?machineID=XXX", + []*schema.UnitState{sus2, sus3}, + }, + { + // Query for nonexistent machine ID should return nothing + "http://example.com/state?machineID=nope", + nil, + }, + { + // Query for specific unit name and specific machine ID should filter by both + "http://example.com/state?unitName=CCC&machineID=XXX", + []*schema.UnitState{sus3}, + }, + } { + fr := registry.NewFakeRegistry() + fr.SetUnitStates([]unit.UnitState{us1, us2, us3, us4}) + fAPI := &client.RegistryClient{fr} + resource := &stateResource{fAPI, "/state"} + rw := httptest.NewRecorder() + req, err := http.NewRequest("GET", tt.url, nil) + if err != nil { + t.Fatalf("case %d: Failed creating http.Request: %v", i, err) + } + + resource.list(rw, req) + if rw.Code != http.StatusOK { + t.Errorf("case %d: Expected 200, got %d", i, rw.Code) + } + ct := rw.HeaderMap["Content-Type"] + if len(ct) != 1 { + t.Errorf("case %d: Response has wrong number of Content-Type values: %v", i, ct) + } else if ct[0] != "application/json" { + t.Errorf("case %d: Expected application/json, got %s", i, ct) + } + + if rw.Body == nil { + t.Errorf("case %d: Received nil response body", i) + continue + } + + var page schema.UnitStatePage + if err := json.Unmarshal(rw.Body.Bytes(), &page); err != nil { + t.Fatalf("case %d: Received unparseable body: %v", i, err) + } + + got := page.States + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("case %d: Unexpected UnitStates received.", i) + t.Logf("Got UnitStates:") + for _, us := range got { + t.Logf("%#v", us) + } + t.Logf("Expected UnitStates:") + for _, us := range tt.expected { + t.Logf("%#v", us) + } + + } + } + fr := registry.NewFakeRegistry() fr.SetUnitStates([]unit.UnitState{ unit.UnitState{UnitName: "XXX", ActiveState: "active"}, diff --git a/registry/fake.go b/registry/fake.go index 12c83c900..127bf32f6 100644 --- a/registry/fake.go +++ b/registry/fake.go @@ -16,7 +16,7 @@ import ( func NewFakeRegistry() *FakeRegistry { return &FakeRegistry{ machines: []machine.MachineState{}, - jobStates: map[string]*unit.UnitState{}, + jobStates: map[string]map[string]*unit.UnitState{}, jobs: map[string]job.Job{}, units: []unit.UnitFile{}, version: nil, @@ -31,7 +31,7 @@ type FakeRegistry struct { sync.RWMutex machines []machine.MachineState - jobStates map[string]*unit.UnitState + jobStates map[string]map[string]*unit.UnitState jobs map[string]job.Job units []unit.UnitFile version *semver.Version @@ -58,10 +58,14 @@ func (f *FakeRegistry) SetUnitStates(states []unit.UnitState) { f.Lock() defer f.Unlock() - f.jobStates = make(map[string]*unit.UnitState, len(states)) + f.jobStates = make(map[string]map[string]*unit.UnitState, len(states)) for _, us := range states { us := us - f.jobStates[us.UnitName] = &us + name := us.UnitName + if _, ok := f.jobStates[name]; !ok { + f.jobStates[name] = make(map[string]*unit.UnitState) + } + f.jobStates[name][us.MachineID] = &us } } @@ -234,7 +238,7 @@ func (f *FakeRegistry) SaveUnitState(jobName string, unitState *unit.UnitState, f.Lock() defer f.Unlock() - f.jobStates[jobName] = unitState + f.jobStates[jobName][unitState.MachineID] = unitState } func (f *FakeRegistry) RemoveUnitState(jobName string) error { @@ -246,16 +250,24 @@ func (f *FakeRegistry) UnitStates() ([]*unit.UnitState, error) { f.Lock() defer f.Unlock() - sortable := make([]string, 0) - for k := range f.jobStates { - sortable = append(sortable, k) - } + var states []*unit.UnitState - sort.Strings(sortable) + // Sort by unit name, then by machineID + sUnitNames := make([]string, 0) + for name := range f.jobStates { + sUnitNames = append(sUnitNames, name) + } + sort.Strings(sUnitNames) - states := make([]*unit.UnitState, len(f.jobStates)) - for i, k := range sortable { - states[i] = f.jobStates[k] + for _, name := range sUnitNames { + sMIDs := make([]string, 0) + for machineID := range f.jobStates[name] { + sMIDs = append(sMIDs, machineID) + } + sort.Strings(sMIDs) + for _, mID := range sMIDs { + states = append(states, f.jobStates[name][mID]) + } } return states, nil