Add host details in API responses (#223)

Add label and pack information for the returned hosts in the single-host
API endpoints.

Example:

```
curl -k 'https://localhost:8080/api/v1/kolide/hosts/7' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX2tleSI6Ii9oNEZ4MUpEVmlvQWhtMC8wNUJKbzZpdldsUDZpMDhjQVBuZXRLeFIvWjNOUGgvMW9VdCsxQnFlNU1CVDVsMlU3ckVGMm5Sb1VxS3ZSUllzSmJJR2lBPT0ifQ.GQQsJgBU3JA1H1o4Y8fPjyfF78F_VY4c9AbrP5k0sCg'
{
  "host": {
    "created_at": "2021-01-16T00:22:33Z",
    "updated_at": "2021-01-16T00:22:51Z",
    "id": 7,
    "detail_updated_at": "1970-01-02T00:00:00Z",
    "label_updated_at": "1970-01-02T00:00:00Z",
    "last_enrolled_at": "2021-01-16T00:22:33Z",
    "seen_time": "2021-01-16T00:22:51Z",
    "hostname": "55d91fc9c303",
    "uuid": "853a4588-0000-0000-a061-7d494d04e9c4",
    "platform": "ubuntu",
    "osquery_version": "4.6.0",
    "os_version": "Ubuntu 20.04.0",
    "build": "",
    "platform_like": "debian",
    "code_name": "",
    "uptime": 0,
    "memory": 16794206208,
    "cpu_type": "x86_64",
    "cpu_subtype": "158",
    "cpu_brand": "Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
    "cpu_physical_cores": 8,
    "cpu_logical_cores": 8,
    "hardware_vendor": "",
    "hardware_model": "",
    "hardware_version": "",
    "hardware_serial": "",
    "computer_name": "55d91fc9c303",
    "primary_ip": "",
    "primary_mac": "",
    "distributed_interval": 10,
    "config_tls_refresh": 0,
    "logger_tls_period": 10,
    "enroll_secret_name": "default",
    "labels": [
      {
        "created_at": "2020-12-22T01:22:47Z",
        "updated_at": "2020-12-22T01:22:47Z",
        "id": 6,
        "name": "All Hosts",
        "description": "All hosts which have enrolled in Fleet",
        "query": "select 1;",
        "label_type": "builtin",
        "label_membership_type": "dynamic"
      }
    ],
    "packs": [
      {
        "created_at": "2021-01-20T16:36:42Z",
        "updated_at": "2021-01-20T16:36:42Z",
        "id": 2,
        "name": "test"
      }
    ],
    "status": "offline",
    "display_text": "55d91fc9c303"
  }
}
```
This commit is contained in:
Zach Wasserman 2021-01-25 13:05:02 -08:00 committed by GitHub
parent 3fc6412e67
commit 6215acdd1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 116 additions and 56 deletions

View file

@ -13,13 +13,10 @@ type HostStatus string
const (
// StatusOnline host is active.
StatusOnline = HostStatus("online")
// StatusOffline no communication with host for OfflineDuration.
StatusOffline = HostStatus("offline")
// StatusMIA no communication with host for MIADuration.
StatusMIA = HostStatus("mia")
// StatusNew means the host has enrolled in the interval defined by
// NewDuration. It is independent of offline and online.
StatusNew = HostStatus("new")
@ -28,7 +25,7 @@ const (
// considered new.
NewDuration = 24 * time.Hour
// OfflineDuration if a host hasn't been in communication for this period it
// MIADuration if a host hasn't been in communication for this period it
// is considered MIA.
MIADuration = 30 * 24 * time.Hour
@ -84,13 +81,13 @@ type HostStore interface {
type HostService interface {
ListHosts(ctx context.Context, opt HostListOptions) (hosts []*Host, err error)
GetHost(ctx context.Context, id uint) (host *Host, err error)
GetHost(ctx context.Context, id uint) (host *HostDetail, err error)
GetHostSummary(ctx context.Context) (summary *HostSummary, err error)
DeleteHost(ctx context.Context, id uint) (err error)
// HostByIdentifier returns one host matching the provided identifier.
// Possible matches can be on osquery_host_identifier, node_key, UUID, or
// hostname.
HostByIdentifier(ctx context.Context, identifier string) (*Host, error)
HostByIdentifier(ctx context.Context, identifier string) (*HostDetail, error)
}
type HostListOptions struct {
@ -146,6 +143,16 @@ type Host struct {
EnrollSecretName string `json:"enroll_secret_name" db:"enroll_secret_name"`
}
// HostDetail provides the full host metadata along with associated labels and
// packs.
type HostDetail struct {
Host
// Labels is the list of labels the host is a member of.
Labels []Label `json:"labels"`
// Packs is the list of packs the host is a member of.
Packs []Pack `json:"packs"`
}
const (
HostKind = "host"
)

View file

@ -169,12 +169,12 @@ type Label struct {
UpdateCreateTimestamps
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Description string `json:"description,omitempty"`
Query string `json:"query"`
Platform string `json:"platform"`
Platform string `json:"platform,omitempty"`
LabelType LabelType `json:"label_type" db:"label_type"`
LabelMembershipType LabelMembershipType `json:"label_membership_type" db:"label_membership_type"`
HostCount int `json:"host_count" db:"host_count"`
HostCount int `json:"host_count,omitempty" db:"host_count"`
}
const (

View file

@ -124,9 +124,9 @@ type Pack struct {
UpdateCreateTimestamps
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Platform string `json:"platform"`
Disabled bool `json:"disabled"`
Description string `json:"description,omitempty"`
Platform string `json:"platform,omitempty"`
Disabled bool `json:"disabled,omitempty"`
}
const (

View file

@ -37,7 +37,7 @@ func (c *Client) GetHosts() ([]HostResponse, error) {
// HostByIdentifier retrieves a host by the uuid, osquery_host_id, hostname, or
// node_key.
func (c *Client) HostByIdentifier(identifier string) (*HostResponse, error) {
func (c *Client) HostByIdentifier(identifier string) (*HostDetailResponse, error) {
response, err := c.AuthenticatedDo("GET", "/api/v1/kolide/hosts/identifier/"+identifier, "", nil)
if err != nil {
return nil, errors.Wrap(err, "GET /api/v1/kolide/hosts/identifier")

View file

@ -4,9 +4,8 @@ import (
"context"
"time"
"github.com/go-kit/kit/endpoint"
"github.com/fleetdm/fleet/server/kolide"
"github.com/pkg/errors"
"github.com/go-kit/kit/endpoint"
)
// HostResponse is the response struct that contains the full host information
@ -27,18 +26,20 @@ func hostResponseForHost(ctx context.Context, svc kolide.Service, host *kolide.H
}, nil
}
func addLabelsToHost(ctx context.Context, svc kolide.Service, host *kolide.Host) (*HostResponse, error) {
labels, err := svc.ListLabelsForHost(ctx, host.ID)
if err != nil {
return nil, errors.Wrap(err, "list labels for host")
}
return &HostResponse{
Host: *host,
// HostDetailresponse is the response struct that contains the full host information
// with the HostDetail details.
type HostDetailResponse struct {
kolide.HostDetail
Status kolide.HostStatus `json:"status"`
DisplayText string `json:"display_text"`
}
func hostDetailResponseForHost(ctx context.Context, svc kolide.Service, host *kolide.HostDetail) (*HostDetailResponse, error) {
return &HostDetailResponse{
HostDetail: *host,
Status: host.Status(time.Now()),
DisplayText: host.HostName,
Labels: labels,
}, nil
}
////////////////////////////////////////////////////////////////////////////////
@ -50,8 +51,8 @@ type getHostRequest struct {
}
type getHostResponse struct {
Host *HostResponse `json:"host"`
Err error `json:"error,omitempty"`
Host *HostDetailResponse `json:"host"`
Err error `json:"error,omitempty"`
}
func (r getHostResponse) error() error { return r.Err }
@ -64,7 +65,7 @@ func makeGetHostEndpoint(svc kolide.Service) endpoint.Endpoint {
return getHostResponse{Err: err}, nil
}
resp, err := hostResponseForHost(ctx, svc, host)
resp, err := hostDetailResponseForHost(ctx, svc, host)
if err != nil {
return getHostResponse{Err: err}, nil
}
@ -76,7 +77,7 @@ func makeGetHostEndpoint(svc kolide.Service) endpoint.Endpoint {
}
////////////////////////////////////////////////////////////////////////////////
// Get Host
// Get Host By Identifier
////////////////////////////////////////////////////////////////////////////////
type hostByIdentifierRequest struct {
@ -91,7 +92,7 @@ func makeHostByIdentifierEndpoint(svc kolide.Service) endpoint.Endpoint {
return getHostResponse{Err: err}, nil
}
resp, err := addLabelsToHost(ctx, svc, host)
resp, err := hostDetailResponseForHost(ctx, svc, host)
if err != nil {
return getHostResponse{Err: err}, nil
}

View file

@ -25,9 +25,9 @@ func (mw loggingMiddleware) ListHosts(ctx context.Context, opt kolide.HostListOp
return hosts, err
}
func (mw loggingMiddleware) GetHost(ctx context.Context, id uint) (*kolide.Host, error) {
func (mw loggingMiddleware) GetHost(ctx context.Context, id uint) (*kolide.HostDetail, error) {
var (
host *kolide.Host
host *kolide.HostDetail
err error
)

View file

@ -4,18 +4,50 @@ import (
"context"
"github.com/fleetdm/fleet/server/kolide"
"github.com/pkg/errors"
)
func (svc service) ListHosts(ctx context.Context, opt kolide.HostListOptions) ([]*kolide.Host, error) {
return svc.ds.ListHosts(opt)
}
func (svc service) GetHost(ctx context.Context, id uint) (*kolide.Host, error) {
return svc.ds.Host(id)
func (svc service) GetHost(ctx context.Context, id uint) (*kolide.HostDetail, error) {
host, err := svc.ds.Host(id)
if err != nil {
return nil, errors.Wrap(err, "get host")
}
return svc.getHostDetails(ctx, host)
}
func (svc service) HostByIdentifier(ctx context.Context, identifier string) (*kolide.Host, error) {
return svc.ds.HostByIdentifier(identifier)
func (svc service) HostByIdentifier(ctx context.Context, identifier string) (*kolide.HostDetail, error) {
host, err := svc.ds.HostByIdentifier(identifier)
if err != nil {
return nil, errors.Wrap(err, "get host by identifier")
}
return svc.getHostDetails(ctx, host)
}
func (svc service) getHostDetails(ctx context.Context, host *kolide.Host) (*kolide.HostDetail, error) {
labels, err := svc.ds.ListLabelsForHost(host.ID)
if err != nil {
return nil, errors.Wrap(err, "get labels for host")
}
packPtrs, err := svc.ds.ListPacksForHost(host.ID)
if err != nil {
return nil, errors.Wrap(err, "get packs for host")
}
// TODO refactor List* APIs to be consistent so we don't have to do this
// transformation
packs := make([]kolide.Pack, 0, len(packPtrs))
for _, p := range packPtrs {
packs = append(packs, *p)
}
return &kolide.HostDetail{Host: *host, Labels: labels, Packs: packs}, nil
}
func (svc service) GetHostSummary(ctx context.Context) (*kolide.HostSummary, error) {

View file

@ -7,7 +7,9 @@ import (
"github.com/fleetdm/fleet/server/config"
"github.com/fleetdm/fleet/server/datastore/inmem"
"github.com/fleetdm/fleet/server/kolide"
"github.com/fleetdm/fleet/server/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestListHosts(t *testing.T) {
@ -33,27 +35,6 @@ func TestListHosts(t *testing.T) {
assert.Len(t, hosts, 1)
}
func TestGetHost(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
assert.Nil(t, err)
svc, err := newTestService(ds, nil, nil)
assert.Nil(t, err)
ctx := context.Background()
host, err := ds.NewHost(&kolide.Host{
HostName: "foo",
})
assert.Nil(t, err)
assert.NotZero(t, host.ID)
hostVerify, err := svc.GetHost(ctx, host.ID)
assert.Nil(t, err)
assert.Equal(t, host.ID, hostVerify.ID)
}
func TestDeleteHost(t *testing.T) {
ds, err := inmem.New(config.TestConfig())
assert.Nil(t, err)
@ -77,3 +58,42 @@ func TestDeleteHost(t *testing.T) {
assert.Len(t, hosts, 0)
}
func TestHostDetails(t *testing.T) {
ds := new(mock.Store)
svc := service{ds: ds}
host := &kolide.Host{ID: 3}
ctx := context.Background()
expectedLabels := []kolide.Label{
{
Name: "foobar",
Description: "the foobar label",
},
}
ds.ListLabelsForHostFunc = func(hid uint) ([]kolide.Label, error) {
return expectedLabels, nil
}
expectedPacks := []kolide.Pack{
{
Name: "pack1",
},
{
Name: "pack2",
},
}
ds.ListPacksForHostFunc = func(hid uint) ([]*kolide.Pack, error) {
packs := []*kolide.Pack{}
for _, p := range expectedPacks {
// Make pointer in inner scope
p2 := p
packs = append(packs, &p2)
}
return packs, nil
}
hostDetail, err := svc.getHostDetails(ctx, host)
require.NoError(t, err)
assert.Equal(t, expectedLabels, hostDetail.Labels)
assert.Equal(t, expectedPacks, hostDetail.Packs)
}