diff --git a/server/kolide/hosts.go b/server/kolide/hosts.go index e393835649..8ea442d589 100644 --- a/server/kolide/hosts.go +++ b/server/kolide/hosts.go @@ -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" ) diff --git a/server/kolide/labels.go b/server/kolide/labels.go index 67936e47ec..8f78c1755a 100644 --- a/server/kolide/labels.go +++ b/server/kolide/labels.go @@ -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 ( diff --git a/server/kolide/packs.go b/server/kolide/packs.go index ba7a8e6ab8..e9257efe7a 100644 --- a/server/kolide/packs.go +++ b/server/kolide/packs.go @@ -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 ( diff --git a/server/service/client_hosts.go b/server/service/client_hosts.go index 450550077e..afaa6ed488 100644 --- a/server/service/client_hosts.go +++ b/server/service/client_hosts.go @@ -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") diff --git a/server/service/endpoint_hosts.go b/server/service/endpoint_hosts.go index 86df9d64b5..3a0eb8ccdf 100644 --- a/server/service/endpoint_hosts.go +++ b/server/service/endpoint_hosts.go @@ -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 } diff --git a/server/service/logging_hosts.go b/server/service/logging_hosts.go index 51303b5eb8..12dba4b9dd 100644 --- a/server/service/logging_hosts.go +++ b/server/service/logging_hosts.go @@ -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 ) diff --git a/server/service/service_hosts.go b/server/service/service_hosts.go index 16cbde8186..e0ecc8dd39 100644 --- a/server/service/service_hosts.go +++ b/server/service/service_hosts.go @@ -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) { diff --git a/server/service/service_hosts_test.go b/server/service/service_hosts_test.go index e66268739a..b41d6d22d0 100644 --- a/server/service/service_hosts_test.go +++ b/server/service/service_hosts_test.go @@ -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) +}