refactor gRPC endpoints with launcher packages in mind (#1582)

The launcher service implementation is an adapter around the TLS service.
All launcher methods that have an equivalent in TLS pass the business logic to the
TLS API.

Closes #1565
This commit is contained in:
Victor Vrantchan
2017-10-24 19:55:32 -04:00
committed by GitHub
parent cbd5187176
commit 5ad562a952
14 changed files with 481 additions and 895 deletions
Generated
+26 -15
View File
@@ -1,6 +1,11 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "git.apache.org/thrift.git"
packages = ["lib/go/thrift"]
revision = "0dd823580c78a79ae9696eb9b3650e400fff140f"
[[projects]]
branch = "master"
name = "github.com/VividCortex/mysqlerr"
@@ -63,7 +68,7 @@
[[projects]]
name = "github.com/go-kit/kit"
packages = ["endpoint","log","metrics","metrics/internal/lv","metrics/prometheus","transport/http"]
packages = ["endpoint","log","metrics","metrics/internal/lv","metrics/prometheus","transport/grpc","transport/http"]
revision = "4dc7be5d2d12881735283bcab7352178e190fc71"
version = "v0.6.0"
@@ -91,6 +96,12 @@
packages = ["proto","ptypes","ptypes/any","ptypes/duration","ptypes/timestamp"]
revision = "130e6b02ab059e7b717a096f397c5b60111cae74"
[[projects]]
name = "github.com/google/uuid"
packages = ["."]
revision = "064e2069ce9c359c118179501254f67d7d37ba24"
version = "0.2"
[[projects]]
name = "github.com/gorilla/context"
packages = ["."]
@@ -139,12 +150,6 @@
revision = "2eee05ed794112d45db504eb05aa693efd2b8b09"
version = "v0.1.0"
[[projects]]
branch = "master"
name = "github.com/kolide/agent-api"
packages = ["."]
revision = "f81f25bd36a2e73a762d4a5192d698bae06a7161"
[[projects]]
branch = "master"
name = "github.com/kolide/goose"
@@ -157,6 +162,18 @@
packages = ["version"]
revision = "566c8f56a6ff7daba204818fbab0f2cb854b3310"
[[projects]]
name = "github.com/kolide/launcher"
packages = ["service","service/internal/launcherproto","service/uuid"]
revision = "b8a2e91e294ee1fb4e39fcc5601d924becc7dbec"
version = "0.3.0"
[[projects]]
branch = "master"
name = "github.com/kolide/osquery-go"
packages = ["gen/osquery","plugin/distributed","plugin/logger"]
revision = "03a792fcca9a86c9a717109c2ad8763d0759105b"
[[projects]]
branch = "master"
name = "github.com/kr/logfmt"
@@ -277,16 +294,10 @@
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/stretchr/objx"
packages = ["."]
revision = "1a9d0bb9f541897e62256577b352fdbc1fb4fd94"
[[projects]]
branch = "master"
name = "github.com/stretchr/testify"
packages = ["assert","mock","require"]
packages = ["assert","require"]
revision = "2aa2c176b9dab406a6970f6a55f513e8a8c8b18f"
[[projects]]
@@ -340,6 +351,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "a7e35edc607087f07c0bba174f1c9d02bf5a616d5f488de2a569e03bab69de68"
inputs-digest = "c4fd9d0e72067244e44c5d04e91a4af2830b38e3609e8984aa24e1c1337286c2"
solver-name = "gps-cdcl"
solver-version = 1
-4
View File
@@ -40,10 +40,6 @@
[[constraint]]
name = "github.com/jmoiron/sqlx"
[[constraint]]
name = "github.com/kolide/agent-api"
branch = "master"
[[constraint]]
name = "github.com/kolide/goose"
branch = "master"
+9 -4
View File
@@ -29,6 +29,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/cobra"
"google.golang.org/grpc"
)
type initializer interface {
@@ -134,9 +135,6 @@ the way that the Fleet server works.
if err != nil {
initFatal(err, "initializing service")
}
// Instantiate a gRPC service to handle launcher requests.
launcher := launcher.New(svc, logger)
defer launcher.GracefulStop()
go func() {
ticker := time.NewTicker(1 * time.Hour)
@@ -201,8 +199,12 @@ the way that the Fleet server works.
healthCheckers[name] = hc
}
}
}
// Instantiate a gRPC service to handle launcher requests.
launcher := launcher.New(svc, logger, grpc.NewServer(), healthCheckers)
r := http.NewServeMux()
r.Handle("/healthz", prometheus.InstrumentHandler("healthz", health.Handler(httpLogger, healthCheckers)))
@@ -269,7 +271,10 @@ the way that the Fleet server works.
<-sig //block on signal
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
errs <- srv.Shutdown(ctx)
errs <- func() error {
launcher.GracefulStop()
return srv.Shutdown(ctx)
}()
}()
logger.Log("terminated", <-errs)
-92
View File
@@ -1,92 +0,0 @@
package launcher
import (
pb "github.com/kolide/agent-api"
"github.com/kolide/fleet/server/kolide"
"golang.org/x/net/context"
)
type authMiddleware struct {
svc kolide.OsqueryService
next pb.ApiServer
}
func newAuthMiddleware(svc kolide.OsqueryService) func(svc pb.ApiServer) pb.ApiServer {
return func(next pb.ApiServer) pb.ApiServer {
return authMiddleware{
svc: svc,
next: next,
}
}
}
func (s authMiddleware) RequestEnrollment(ctx context.Context, req *pb.EnrollmentRequest) (*pb.EnrollmentResponse, error) {
return s.next.RequestEnrollment(ctx, req)
}
func (s authMiddleware) RequestConfig(ctx context.Context, req *pb.AgentApiRequest) (*pb.ConfigResponse, error) {
authCtx, auth, err := s.authenticateHost(ctx, req.NodeKey)
if err != nil {
return nil, err
}
if auth.nodeInvalid {
return &pb.ConfigResponse{NodeInvalid: auth.nodeInvalid, ErrorCode: auth.errorCode}, nil
}
return s.next.RequestConfig(authCtx, req)
}
func (s authMiddleware) RequestQueries(ctx context.Context, req *pb.AgentApiRequest) (resp *pb.QueryCollection, err error) {
authCtx, auth, err := s.authenticateHost(ctx, req.NodeKey)
if err != nil {
return nil, err
}
if auth.nodeInvalid {
return &pb.QueryCollection{NodeInvalid: auth.nodeInvalid, ErrorCode: auth.errorCode}, nil
}
return s.next.RequestQueries(authCtx, req)
}
func (s authMiddleware) PublishLogs(ctx context.Context, req *pb.LogCollection) (resp *pb.AgentApiResponse, err error) {
authCtx, auth, err := s.authenticateHost(ctx, req.NodeKey)
if err != nil {
return nil, err
}
if auth.nodeInvalid {
return &pb.AgentApiResponse{NodeInvalid: auth.nodeInvalid, ErrorCode: auth.errorCode}, nil
}
return s.next.PublishLogs(authCtx, req)
}
func (s authMiddleware) PublishResults(ctx context.Context, req *pb.ResultCollection) (resp *pb.AgentApiResponse, err error) {
authCtx, auth, err := s.authenticateHost(ctx, req.NodeKey)
if err != nil {
return nil, err
}
if auth.nodeInvalid {
return &pb.AgentApiResponse{NodeInvalid: auth.nodeInvalid, ErrorCode: auth.errorCode}, nil
}
return s.next.PublishResults(authCtx, req)
}
func (s authMiddleware) CheckHealth(ctx context.Context, coll *pb.AgentApiRequest) (*pb.HealthCheckResponse, error) {
// there should not be any auth
return s.next.CheckHealth(ctx, coll)
}
type auth struct {
nodeInvalid bool
errorCode string
host *kolide.Host
}
func (s authMiddleware) authenticateHost(ctx context.Context, nodeKey string) (context.Context, *auth, error) {
host, err := s.svc.AuthenticateHost(newCtx(ctx), nodeKey)
if err != nil {
if errEnroll, ok := err.(enrollmentError); ok {
return ctx, &auth{nodeInvalid: errEnroll.NodeInvalid(), errorCode: errEnroll.Error()}, nil
}
return nil, nil, err
}
return withHost(ctx, *host), &auth{host: host}, nil
}
-76
View File
@@ -1,76 +0,0 @@
package launcher
import (
"context"
"testing"
pb "github.com/kolide/agent-api"
"github.com/kolide/fleet/server/contexts/host"
"github.com/kolide/fleet/server/kolide"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
var authTestHost = &kolide.Host{HostName: "jimmy"}
var nullHost *kolide.Host
func TestAuthRequestConfig(t *testing.T) {
mockSvc := new(mockOsqueryService)
mockSvc.On(
"AuthenticateHost",
oldContext,
"nodekey",
).Return(
authTestHost,
nil,
)
mockSvc.On(
"GetClientConfig",
mock.MatchedBy(func(ctx context.Context) bool {
if h, ok := host.FromContext(ctx); ok {
return h.HostName == authTestHost.HostName
}
return false
}),
).Return(
&kolide.OsqueryConfig{},
nil,
)
svr := newAuthMiddleware(mockSvc)(&agentBinding{mockSvc})
resp, err := svr.RequestConfig(oldContext, &pb.AgentApiRequest{NodeKey: "nodekey"})
mockSvc.AssertExpectations(t)
require.Nil(t, err)
require.NotNil(t, resp)
}
func TestAuthFailRequestConfig(t *testing.T) {
cxtMatcher := mock.MatchedBy(func(ctx context.Context) bool {
if h, ok := hostFromContext(ctx); ok {
return h.HostName == authTestHost.HostName
}
return false
})
mockSvc := new(mockOsqueryService)
mockSvc.On(
"AuthenticateHost",
oldContext,
"nodekey",
).Return(
nullHost,
&mockEnrollError{},
)
mockSvc.On(
"GetClientConfig",
cxtMatcher,
).Return(
&kolide.OsqueryConfig{},
nil,
)
svr := newAuthMiddleware(mockSvc)(&agentBinding{mockSvc})
resp, err := svr.RequestConfig(oldContext, &pb.AgentApiRequest{NodeKey: "nodekey"})
mockSvc.AssertNotCalled(t, "GetClientConfig", newContext)
require.Nil(t, err)
require.NotNil(t, resp)
assert.True(t, resp.NodeInvalid)
}
-177
View File
@@ -1,177 +0,0 @@
package launcher
import (
"bytes"
"encoding/json"
"strconv"
pb "github.com/kolide/agent-api"
"github.com/kolide/fleet/server/kolide"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
var errNotImplmented = errors.New("not implemented")
// agentBinding implements ApiClient interface and maps gRPC domain functions to the application.
type agentBinding struct {
service kolide.OsqueryService
}
func newAgentBinding(svc kolide.OsqueryService) pb.ApiServer {
return &agentBinding{
service: svc,
}
}
type enrollmentError interface {
NodeInvalid() bool
Error() string
}
// Attempt to enroll a host with kolide/cloud
func (b *agentBinding) RequestEnrollment(ctx context.Context, req *pb.EnrollmentRequest) (*pb.EnrollmentResponse, error) {
var resp pb.EnrollmentResponse
nodeKey, err := b.service.EnrollAgent(newCtx(ctx), req.EnrollSecret, req.HostIdentifier)
if err != nil {
if errEnroll, ok := err.(enrollmentError); ok {
resp.NodeInvalid = errEnroll.NodeInvalid()
resp.ErrorCode = errEnroll.Error()
return &resp, nil
}
return nil, err
}
resp.NodeKey = nodeKey
return &resp, nil
}
// RequestConfig requests an updated configuration
func (b *agentBinding) RequestConfig(ctx context.Context, req *pb.AgentApiRequest) (*pb.ConfigResponse, error) {
config, err := b.service.GetClientConfig(newCtx(ctx))
if err != nil {
return nil, err
}
// Launcher manages plugins so remove them from configuration if they exist.
for _, optionName := range []string{"distributed_plugin", "logger_plugin"} {
if _, ok := config.Options[optionName]; ok {
delete(config.Options, optionName)
}
}
var writer bytes.Buffer
if err = json.NewEncoder(&writer).Encode(config); err != nil {
return nil, err
}
return &pb.ConfigResponse{ConfigJsonBlob: writer.String()}, nil
}
// RequestQueries request/pull distributed queries
func (b *agentBinding) RequestQueries(ctx context.Context, _ *pb.AgentApiRequest) (*pb.QueryCollection, error) {
queryMap, _, err := b.service.GetDistributedQueries(newCtx(ctx))
if err != nil {
return nil, err
}
var result pb.QueryCollection
for id, query := range queryMap {
result.Queries = append(result.Queries, &pb.QueryCollection_Query{Id: id, Query: query})
}
return &result, nil
}
// StatusLog handles osquery logging messages
type StatusLog struct {
Severity string `json:"s"`
Filename string `json:"f"`
Line string `json:"i"`
Message string `json:"m"`
}
// convert the json from grpc client to an object suitable
// for consumption by fleet
func toKolideLog(jsn string) (*kolide.OsqueryStatusLog, error) {
var status StatusLog
err := json.NewDecoder(bytes.NewBufferString(jsn)).Decode(&status)
if err != nil {
return nil, err
}
result := &kolide.OsqueryStatusLog{
Severity: status.Severity,
Filename: status.Filename,
Line: status.Line,
Message: status.Message,
}
return result, nil
}
// PublishLogs publish logs from osqueryd
func (b *agentBinding) PublishLogs(ctx context.Context, coll *pb.LogCollection) (*pb.AgentApiResponse, error) {
handler := func(_ context.Context, _ *pb.LogCollection) error { return nil }
switch coll.LogType {
case pb.LogCollection_RESULT:
handler = b.handleResultLogs
case pb.LogCollection_STATUS:
handler = b.handleStatusLogs
}
if err := handler(ctx, coll); err != nil {
return nil, err
}
return &pb.AgentApiResponse{}, nil
}
func (b *agentBinding) handleResultLogs(ctx context.Context, coll *pb.LogCollection) error {
var results []kolide.OsqueryResultLog
for _, log := range coll.Logs {
var result kolide.OsqueryResultLog
if err := json.Unmarshal([]byte(log.Data), &result); err != nil {
return errors.Wrap(err, "unmarshaling result log")
}
results = append(results, result)
}
if err := b.service.SubmitResultLogs(newCtx(ctx), results); err != nil {
return errors.Wrap(err, "submitting status logs")
}
return nil
}
func (b *agentBinding) handleStatusLogs(ctx context.Context, coll *pb.LogCollection) error {
var statuses []kolide.OsqueryStatusLog
for _, record := range coll.Logs {
status, err := toKolideLog(record.Data)
if err != nil {
return errors.Wrap(err, "decoding status log")
}
statuses = append(statuses, *status)
}
if err := b.service.SubmitStatusLogs(newCtx(ctx), statuses); err != nil {
return errors.Wrap(err, "submitting status logs")
}
return nil
}
// PublishResults publish distributed query results
func (b *agentBinding) PublishResults(ctx context.Context, coll *pb.ResultCollection) (*pb.AgentApiResponse, error) {
results := kolide.OsqueryDistributedQueryResults{}
statuses := map[string]string{}
for _, result := range coll.Results {
statuses[result.Id] = strconv.Itoa(int(result.Status))
rows := []map[string]string{}
for _, row := range result.Rows {
cols := map[string]string{}
for _, colVal := range row.Columns {
cols[colVal.Name] = colVal.Value
}
if len(cols) == 0 {
continue
}
rows = append(rows, cols)
}
results[result.Id] = rows
}
if err := b.service.SubmitDistributedQueryResults(newCtx(ctx), results, statuses); err != nil {
return nil, errors.Wrap(err, "submitting distributed query results")
}
return &pb.AgentApiResponse{}, nil
}
func (svc *agentBinding) CheckHealth(ctx context.Context, coll *pb.AgentApiRequest) (*pb.HealthCheckResponse, error) {
return nil, errNotImplmented
}
-391
View File
@@ -1,391 +0,0 @@
package launcher
import (
newctx "context"
"errors"
"testing"
pb "github.com/kolide/agent-api"
"github.com/kolide/fleet/server/kolide"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
type mockEnrollError struct{}
func (ee *mockEnrollError) NodeInvalid() bool { return true }
func (ee *mockEnrollError) Error() string { return "enroll failed" }
type mockOsqueryService struct {
mock.Mock
}
func (m *mockOsqueryService) EnrollAgent(ctx newctx.Context, enrollSecret, hostIdentifier string) (string, error) {
args := m.Called(ctx, enrollSecret, hostIdentifier)
return args.String(0), args.Error(1)
}
func (m *mockOsqueryService) AuthenticateHost(ctx newctx.Context, nodeKey string) (*kolide.Host, error) {
args := m.Called(ctx, nodeKey)
return args.Get(0).(*kolide.Host), args.Error(1)
}
func (m *mockOsqueryService) GetClientConfig(ctx newctx.Context) (*kolide.OsqueryConfig, error) {
args := m.Called(ctx)
return args.Get(0).(*kolide.OsqueryConfig), args.Error(1)
}
func (m *mockOsqueryService) GetDistributedQueries(ctx newctx.Context) (map[string]string, uint, error) {
args := m.Called(ctx)
return args.Get(0).(map[string]string), args.Get(1).(uint), args.Error(2)
}
func (m *mockOsqueryService) SubmitDistributedQueryResults(ctx newctx.Context, results kolide.OsqueryDistributedQueryResults, statuses map[string]string) error {
args := m.Called(ctx, results, statuses)
return args.Error(0)
}
func (m *mockOsqueryService) SubmitStatusLogs(ctx newctx.Context, logs []kolide.OsqueryStatusLog) error {
args := m.Called(ctx, logs)
return args.Error(0)
}
func (m *mockOsqueryService) SubmitResultLogs(ctx newctx.Context, logs []kolide.OsqueryResultLog) error {
args := m.Called(ctx, logs)
return args.Error(0)
}
var oldContext = context.Background()
var newContext = newctx.Background()
var errMockEnrollError = &mockEnrollError{}
var errTestError = errors.New("test error")
func TestRequestEnrollementHappyPath(t *testing.T) {
request := &pb.EnrollmentRequest{
EnrollSecret: "supersecret",
HostIdentifier: "somehost",
}
mockSvc := new(mockOsqueryService)
mockSvc.On(
"EnrollAgent",
newctx.Background(),
request.EnrollSecret,
request.HostIdentifier,
).Return(
"nodekey",
nil,
)
agent := agentBinding{
service: mockSvc,
}
resp, err := agent.RequestEnrollment(oldContext, request)
mockSvc.AssertExpectations(t)
assert.Nil(t, err)
assert.Equal(t, "nodekey", resp.NodeKey)
assert.False(t, resp.NodeInvalid)
}
func TestRequestEnrollmentFailed(t *testing.T) {
request := &pb.EnrollmentRequest{
EnrollSecret: "supersecret",
HostIdentifier: "somehost",
}
mockSvc := new(mockOsqueryService)
mockSvc.On(
"EnrollAgent",
newctx.Background(),
request.EnrollSecret,
request.HostIdentifier,
).Return(
"",
errMockEnrollError,
)
agent := agentBinding{
service: mockSvc,
}
resp, err := agent.RequestEnrollment(oldContext, request)
mockSvc.AssertExpectations(t)
assert.Nil(t, err)
assert.True(t, resp.NodeInvalid)
}
func TestRequestEnrollmentError(t *testing.T) {
request := &pb.EnrollmentRequest{
EnrollSecret: "supersecret",
HostIdentifier: "somehost",
}
mockSvc := new(mockOsqueryService)
mockSvc.On(
"EnrollAgent",
newctx.Background(),
request.EnrollSecret,
request.HostIdentifier,
).Return(
"",
errTestError,
)
agent := agentBinding{
service: mockSvc,
}
_, err := agent.RequestEnrollment(oldContext, request)
mockSvc.AssertExpectations(t)
require.NotNil(t, err)
assert.Equal(t, errTestError, err)
}
func TestRequestConfigHappyPath(t *testing.T) {
request := &pb.AgentApiRequest{
NodeKey: "nodekey",
}
mockSvc := new(mockOsqueryService)
mockSvc.On(
"GetClientConfig",
newctx.Background(),
).Return(
&kolide.OsqueryConfig{
Options: map[string]interface{}{
"option1": "optionval",
"distributed_plugin": "tls",
},
Decorators: kolide.Decorators{
Load: []string{
"SELECT * FROM users u JOIN groups g WHERE u.gid = g.gid",
},
},
},
nil,
)
agent := agentBinding{
service: mockSvc,
}
resp, err := agent.RequestConfig(oldContext, request)
mockSvc.AssertExpectations(t)
assert.Nil(t, err)
// verify distributed_plugin was removed
expectedJSON := "{\"options\":{\"option1\":\"optionval\"},\"decorators\":{\"load\":[\"SELECT * FROM users u JOIN groups g WHERE u.gid = g.gid\"]}}\n"
require.NotNil(t, resp)
assert.Equal(t, expectedJSON, resp.ConfigJsonBlob)
assert.False(t, resp.NodeInvalid)
}
func TestRequestConfigError(t *testing.T) {
request := &pb.AgentApiRequest{
NodeKey: "nodekey",
}
var nilConfig *kolide.OsqueryConfig
mockSvc := new(mockOsqueryService)
mockSvc.On(
"GetClientConfig",
newctx.Background(),
).Return(
nilConfig,
errTestError,
)
agent := agentBinding{
service: mockSvc,
}
resp, err := agent.RequestConfig(oldContext, request)
mockSvc.AssertExpectations(t)
require.NotNil(t, err)
assert.Equal(t, errTestError, err)
assert.Nil(t, resp)
}
func TestRequestQueriesHappyPath(t *testing.T) {
mockSvc := new(mockOsqueryService)
mockSvc.On(
"GetDistributedQueries",
newctx.Background(),
).Return(
map[string]string{
"query1": "select * from foo;",
},
uint(0),
nil,
)
agent := agentBinding{mockSvc}
qc, err := agent.RequestQueries(oldContext, nil)
mockSvc.AssertExpectations(t)
require.Nil(t, err)
require.NotNil(t, qc)
assert.Len(t, qc.Queries, 1)
}
func TestToKolideLog(t *testing.T) {
jsn := "{\"s\":\"0\",\"f\":\"scheduler.cpp\",\"i\":\"73\",\"m\":\"Executing scheduled query pack\\/xxx\\/services: select name, port, protocol from etc_services;\",\"h\":\"DE56C776-2F5A-56DF-81C7-F64EE1BBEC8C\",\"c\":\"Fri Aug 11 22:32:27 2017 UTC\",\"u\":\"1502490747\"}"
sl, err := toKolideLog(jsn)
require.Nil(t, err, "unexpected error")
assert.Equal(t, "0", sl.Severity, "severity mismatch")
assert.Equal(t, "scheduler.cpp", sl.Filename, "file name mismatch")
assert.Equal(t, "73", sl.Line, "line number mismatch")
malformedJSON := "{\"s\":\"0,\"f\":\"scheduler.cpp\",\"i\":\"73\",\"m\":\"Executing scheduled query pack\\/xxx\\/services: select name, port, protocol from etc_services;\",\"h\":\"DE56C776-2F5A-56DF-81C7-F64EE1BBEC8C\",\"c\":\"Fri Aug 11 22:32:27 2017 UTC\",\"u\":\"1502490747\"}"
sl, err = toKolideLog(malformedJSON)
assert.NotNil(t, err, "malformed json should have erred")
assert.Nil(t, sl, "result should be nil on err")
}
func TestPublishStatusLogs(t *testing.T) {
statusJSON := "{\"s\":\"0\",\"f\":\"scheduler.cpp\",\"i\":\"73\",\"m\":\"Executing scheduled query pack\\/xxx\\/services: select name, port, protocol from etc_services;\",\"h\":\"DE56C776-2F5A-56DF-81C7-F64EE1BBEC8C\",\"c\":\"Fri Aug 11 22:32:27 2017 UTC\",\"u\":\"1502490747\"}"
statusLogCol := &pb.LogCollection{
LogType: pb.LogCollection_STATUS,
Logs: []*pb.LogCollection_Log{
&pb.LogCollection_Log{
Data: statusJSON,
},
},
}
statuses := []kolide.OsqueryStatusLog{
kolide.OsqueryStatusLog{
Severity: "0",
Filename: "scheduler.cpp",
Line: "73",
Message: `Executing scheduled query pack/xxx/services: select name, port, protocol from etc_services;`,
},
}
mockSvc := new(mockOsqueryService)
mockSvc.On(
"SubmitStatusLogs",
newContext,
statuses,
).Return(
nil,
)
agent := agentBinding{mockSvc}
resp, err := agent.PublishLogs(oldContext, statusLogCol)
mockSvc.AssertExpectations(t)
mockSvc.AssertCalled(t, "SubmitStatusLogs", newContext, statuses)
require.Nil(t, err)
require.NotNil(t, resp)
assert.False(t, resp.NodeInvalid)
}
func TestPublishStatusLogsUnhandledLogType(t *testing.T) {
statusJSON := "{\"s\":\"0\",\"f\":\"scheduler.cpp\",\"i\":\"73\",\"m\":\"Executing scheduled query pack\\/xxx\\/services: select name, port, protocol from etc_services;\",\"h\":\"DE56C776-2F5A-56DF-81C7-F64EE1BBEC8C\",\"c\":\"Fri Aug 11 22:32:27 2017 UTC\",\"u\":\"1502490747\"}"
statusLogCol := &pb.LogCollection{
LogType: pb.LogCollection_AGENT,
Logs: []*pb.LogCollection_Log{
&pb.LogCollection_Log{
Data: statusJSON,
},
},
}
statuses := []kolide.OsqueryStatusLog{
kolide.OsqueryStatusLog{
Severity: "0",
Filename: "scheduler.cpp",
Line: "73",
Message: `Executing scheduled query pack/xxx/services: select name, port, protocol from etc_services;`,
},
}
mockSvc := new(mockOsqueryService)
mockSvc.On(
"SubmitStatusLogs",
newContext,
statuses,
).Return(
nil,
)
agent := agentBinding{mockSvc}
resp, err := agent.PublishLogs(oldContext, statusLogCol)
mockSvc.AssertNotCalled(t, "SubmitStatusLogs", newContext, statuses)
require.Nil(t, err)
require.NotNil(t, resp)
assert.False(t, resp.NodeInvalid)
}
func TestPublishResultLogs(t *testing.T) {
resultJSON := "{\"name\":\"pack\\/xxx\\/services\",\"hostIdentifier\":\"DE56C776-2F5A-56DF-81C7-F64EE1BBEC8C\",\"calendarTime\":\"Fri Aug 11 22:16:45 2017 UTC\",\"unixTime\":\"1502489805\",\"decorations\":{\"host_uuid\":\"DE56C776-2F5A-56DF-81C7-F64EE1BBEC8C\",\"hostname\":\"Johns-MacBook-Pro.local\"},\"columns\":{\"name\":\"ms-dotnetster\",\"port\":\"3126\",\"protocol\":\"udp\"},\"action\":\"added\"}"
resultLogColl := &pb.LogCollection{
LogType: pb.LogCollection_RESULT,
Logs: []*pb.LogCollection_Log{
&pb.LogCollection_Log{
Data: resultJSON,
},
},
}
results := []kolide.OsqueryResultLog{
kolide.OsqueryResultLog{
Name: "pack/xxx/services",
HostIdentifier: "DE56C776-2F5A-56DF-81C7-F64EE1BBEC8C",
UnixTime: "1502489805",
CalendarTime: "Fri Aug 11 22:16:45 2017 UTC",
Columns: map[string]string{
"name": "ms-dotnetster",
"port": "3126",
"protocol": "udp",
},
Action: "added",
Decorations: map[string]string{
"host_uuid": "DE56C776-2F5A-56DF-81C7-F64EE1BBEC8C",
"hostname": "Johns-MacBook-Pro.local",
},
},
}
mockSvc := new(mockOsqueryService)
mockSvc.On(
"SubmitResultLogs",
newContext,
results,
).Return(
nil,
)
agent := agentBinding{mockSvc}
resp, err := agent.PublishLogs(oldContext, resultLogColl)
mockSvc.AssertExpectations(t)
require.Nil(t, err)
require.NotNil(t, resp)
}
func TestPublishResults(t *testing.T) {
coll := &pb.ResultCollection{
NodeKey: "somekey",
Results: []*pb.ResultCollection_Result{
&pb.ResultCollection_Result{
Id: "myquery",
Status: 0,
Rows: []*pb.ResultCollection_Result_ResultRow{
&pb.ResultCollection_Result_ResultRow{
Columns: []*pb.ResultCollection_Result_ResultRow_Column{
&pb.ResultCollection_Result_ResultRow_Column{
Name: "aColumn",
Value: "aValue",
},
},
},
},
},
},
}
results := kolide.OsqueryDistributedQueryResults{
"myquery": []map[string]string{
map[string]string{
"aColumn": "aValue",
},
},
}
statuses := map[string]string{
"myquery": "0",
}
mockSvc := new(mockOsqueryService)
mockSvc.On(
"SubmitDistributedQueryResults",
newContext,
results,
statuses,
).Return(
nil,
)
agent := agentBinding{mockSvc}
resp, err := agent.PublishResults(oldContext, coll)
mockSvc.AssertExpectations(t)
require.Nil(t, err)
require.NotNil(t, resp)
}
-33
View File
@@ -1,33 +0,0 @@
package launcher
import (
newcontext "context"
"github.com/kolide/fleet/server/contexts/host"
"github.com/kolide/fleet/server/kolide"
old "golang.org/x/net/context"
)
type contextKey int
const hostKey contextKey = 0
// newCtx is used to map the old golang.com/net/context which we are forced to use
// because our generated gRPC code uses it, to the new stdlib context, which is used
// by the Fleet application.
func newCtx(ctx old.Context) newcontext.Context {
if h, ok := ctx.Value(hostKey).(kolide.Host); ok {
return host.NewContext(newcontext.Background(), h)
}
return newcontext.Background()
}
// withHost creates a golang.org/x/net/context containing a host
func withHost(ctx old.Context, h kolide.Host) old.Context {
return old.WithValue(ctx, hostKey, h)
}
func hostFromContext(ctx old.Context) (kolide.Host, bool) {
h, ok := ctx.Value(hostKey).(kolide.Host)
return h, ok
}
+178
View File
@@ -0,0 +1,178 @@
package launcher
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/go-kit/kit/log"
"github.com/kolide/osquery-go/plugin/distributed"
"github.com/kolide/osquery-go/plugin/logger"
"github.com/pkg/errors"
"github.com/kolide/fleet/server/contexts/host"
"github.com/kolide/fleet/server/health"
"github.com/kolide/fleet/server/kolide"
)
// launcherWrapper wraps the TLS interface.
type launcherWrapper struct {
tls kolide.OsqueryService
logger log.Logger
healthCheckers map[string]health.Checker
}
func (svc *launcherWrapper) RequestEnrollment(ctx context.Context, enrollSecret, hostIdentifier string) (string, bool, error) {
nodeKey, err := svc.tls.EnrollAgent(ctx, enrollSecret, hostIdentifier)
if err != nil {
if authErr, ok := err.(nodeInvalidErr); ok {
return "", authErr.NodeInvalid(), err
}
return "", false, err
}
return nodeKey, false, nil
}
func (svc *launcherWrapper) RequestConfig(ctx context.Context, nodeKey string) (string, bool, error) {
newCtx, invalid, err := svc.authenticateHost(ctx, nodeKey)
if err != nil {
return "", invalid, err
}
config, err := svc.tls.GetClientConfig(newCtx)
if err != nil {
return "", false, errors.Wrap(err, "get config for launcher")
}
// Launcher manages plugins so remove them from configuration if they exist.
for _, optionName := range []string{"distributed_plugin", "logger_plugin"} {
if _, ok := config.Options[optionName]; ok {
delete(config.Options, optionName)
}
}
configJSON, err := json.Marshal(config)
if err != nil {
return "", false, errors.Wrap(err, "encoding config for launcher")
}
return string(configJSON), false, nil
}
func (svc *launcherWrapper) RequestQueries(ctx context.Context, nodeKey string) (*distributed.GetQueriesResult, bool, error) {
newCtx, invalid, err := svc.authenticateHost(ctx, nodeKey)
if err != nil {
return nil, invalid, err
}
queryMap, accelerate, err := svc.tls.GetDistributedQueries(newCtx)
if err != nil {
return nil, false, errors.Wrap(err, "get queries for launcher")
}
result := &distributed.GetQueriesResult{
Queries: queryMap,
AccelerateSeconds: int(accelerate),
}
return result, false, nil
}
func (svc *launcherWrapper) PublishLogs(ctx context.Context, nodeKey string, logType logger.LogType, logs []string) (string, string, bool, error) {
newCtx, invalid, err := svc.authenticateHost(ctx, nodeKey)
if err != nil {
return "", "", invalid, errors.Wrap(err, "authenticate launcher")
}
switch logType {
case logger.LogTypeStatus:
var statuses []kolide.OsqueryStatusLog
for _, log := range logs {
// StatusLog handles osquery logging messages
var statusLog = struct {
Severity string `json:"s"`
Filename string `json:"f"`
Line string `json:"i"`
Message string `json:"m"`
}{}
if err := json.Unmarshal([]byte(log), &statusLog); err != nil {
return "", "", false, errors.Wrap(err, "decode status log from launcher")
}
statuses = append(statuses, kolide.OsqueryStatusLog{
Severity: statusLog.Severity,
Filename: statusLog.Filename,
Line: statusLog.Line,
Message: statusLog.Message,
})
}
err = svc.tls.SubmitStatusLogs(newCtx, statuses)
return "", "", false, errors.Wrap(err, "submit status logs from launcher")
case logger.LogTypeSnapshot, logger.LogTypeString:
var results []kolide.OsqueryResultLog
for _, log := range logs {
var result kolide.OsqueryResultLog
if err := json.Unmarshal([]byte(log), &result); err != nil {
return "", "", false, errors.Wrap(err, "unmarshaling result log")
}
results = append(results, result)
}
err = svc.tls.SubmitResultLogs(newCtx, results)
return "", "", false, errors.Wrap(err, "submit result logs from launcher")
default:
// We have a logTypeAgent which is not there in the osquery-go enum.
// See https://github.com/kolide/launcher/issues/183
panic(fmt.Sprintf("%s log type not implemented", logType))
}
}
func (svc *launcherWrapper) PublishResults(ctx context.Context, nodeKey string, results []distributed.Result) (string, string, bool, error) {
newCtx, invalid, err := svc.authenticateHost(ctx, nodeKey)
if err != nil {
return "", "", invalid, err
}
osqueryResults := make(kolide.OsqueryDistributedQueryResults, len(results))
statuses := make(map[string]string, len(results))
for _, result := range results {
statuses[result.QueryName] = strconv.Itoa(result.Status)
osqueryResults[result.QueryName] = result.Rows
}
err = svc.tls.SubmitDistributedQueryResults(newCtx, osqueryResults, statuses)
return "", "", false, errors.Wrap(err, "submit launcher results")
}
func (svc *launcherWrapper) CheckHealth(ctx context.Context) (int32, error) {
healthy := health.CheckHealth(svc.logger, svc.healthCheckers)
if !healthy {
return 1, nil
}
return 0, nil
}
// authenticateHost verifies the host node key using the TLS API and returns back a
// context which includes the host as a context value.
// In the kolide.OsqueryService authentication is done via endpoint middleware, but all launcher endpoints require
// an explicit return for NodeInvalid, so we check in this helper method instead.
func (svc *launcherWrapper) authenticateHost(ctx context.Context, nodeKey string) (context.Context, bool, error) {
node, err := svc.tls.AuthenticateHost(ctx, nodeKey)
if err != nil {
if authErr, ok := err.(nodeInvalidErr); ok {
return ctx, authErr.NodeInvalid(), err
}
return ctx, false, err
}
ctx = host.NewContext(ctx, *node)
return ctx, false, nil
}
type nodeInvalidErr interface {
error
NodeInvalid() bool
}
+155
View File
@@ -0,0 +1,155 @@
package launcher
import (
"context"
"testing"
"github.com/go-kit/kit/log"
"github.com/kolide/fleet/server/health"
"github.com/kolide/fleet/server/kolide"
"github.com/kolide/fleet/server/mock"
"github.com/kolide/osquery-go/plugin/distributed"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLauncherEnrollment(t *testing.T) {
launcher, tls := newTestService(t)
ctx := context.Background()
nodeKey, invalid, err := launcher.RequestEnrollment(ctx, "secret", "identifier")
require.Nil(t, err)
assert.True(t, tls.EnrollAgentFuncInvoked)
assert.False(t, invalid)
assert.Equal(t, "noop", nodeKey)
}
func TestLauncherRequestConfig(t *testing.T) {
launcher, tls := newTestService(t)
ctx := context.Background()
config, invalid, err := launcher.RequestConfig(ctx, "noop")
require.Nil(t, err)
assert.True(t, tls.AuthenticateHostFuncInvoked)
assert.False(t, invalid)
assert.Equal(t, `{"options":{"key":"value"},"decorators":{}}`, config)
}
func TestLauncherRequestQueries(t *testing.T) {
launcher, tls := newTestService(t)
ctx := context.Background()
result, invalid, err := launcher.RequestQueries(ctx, "noop")
require.Nil(t, err)
assert.True(t, tls.AuthenticateHostFuncInvoked)
assert.False(t, invalid)
assert.Equal(t, map[string]string{"noop": `{"key": "value"}`}, result.Queries)
}
func TestLauncherPublishResults(t *testing.T) {
launcher, tls := newTestService(t)
ctx := context.Background()
_, _, invalid, err := launcher.PublishResults(
ctx,
"noop",
[]distributed.Result{},
)
require.Nil(t, err)
assert.True(t, tls.AuthenticateHostFuncInvoked)
assert.False(t, invalid)
// test with result
var result = map[string]string{"key": "value"}
tls.SubmitDistributedQueryResultsFunc = func(
ctx context.Context,
results kolide.OsqueryDistributedQueryResults,
statuses map[string]string) (err error) {
assert.Equal(t, results["query"][0], result)
return nil
}
_, _, invalid, err = launcher.PublishResults(
ctx,
"noop",
[]distributed.Result{
{
QueryName: "query",
Status: 1,
Rows: []map[string]string{result},
},
},
)
require.Nil(t, err)
assert.False(t, invalid)
}
func newTestService(t *testing.T) (*launcherWrapper, *mock.TLSService) {
tls := newTLSService(t)
launcher := &launcherWrapper{
tls: tls,
logger: log.NewNopLogger(),
healthCheckers: map[string]health.Checker{
"noop": health.Nop(),
},
}
return launcher, tls
}
// NewTLS service returns a mock TLS service where all the methods have a noop implementation.
// To test additional behaviors, override the funcs on the TLSService struct.
func newTLSService(t *testing.T) *mock.TLSService {
return &mock.TLSService{
EnrollAgentFunc: func(
ctx context.Context,
enrollSecret string,
hostIdentifier string,
) (nodeKey string, err error) {
nodeKey = "noop"
return
},
AuthenticateHostFunc: func(
ctx context.Context,
nodeKey string,
) (host *kolide.Host, err error) {
return &kolide.Host{
NodeKey: nodeKey,
}, nil
},
GetClientConfigFunc: func(
ctx context.Context,
) (config *kolide.OsqueryConfig, err error) {
return &kolide.OsqueryConfig{
Options: map[string]interface{}{
"key": "value",
},
}, nil
},
GetDistributedQueriesFunc: func(
ctx context.Context,
) (queries map[string]string, accelerate uint, err error) {
queries = map[string]string{
"noop": `{"key": "value"}`,
}
return
},
SubmitDistributedQueryResultsFunc: func(
ctx context.Context,
results kolide.OsqueryDistributedQueryResults,
statuses map[string]string,
) (err error) {
return
},
SubmitStatusLogsFunc: func(ctx context.Context, logs []kolide.OsqueryStatusLog) (err error) {
return
},
SubmitResultLogsFunc: func(ctx context.Context, logs []kolide.OsqueryResultLog) (err error) {
return
},
}
}
-89
View File
@@ -1,89 +0,0 @@
package launcher
import (
"time"
kitlog "github.com/go-kit/kit/log"
pb "github.com/kolide/agent-api"
"golang.org/x/net/context"
)
type loggingMiddleware struct {
logger kitlog.Logger
next pb.ApiServer
}
func newLoggingMiddleware(logger kitlog.Logger) func(svc pb.ApiServer) pb.ApiServer {
return func(next pb.ApiServer) pb.ApiServer {
return loggingMiddleware{
logger: kitlog.With(logger, "component", "gRPC Launcher"),
next: next,
}
}
}
func (s loggingMiddleware) RequestEnrollment(ctx context.Context, req *pb.EnrollmentRequest) (resp *pb.EnrollmentResponse, err error) {
defer func(begin time.Time) {
s.logger.Log(
"method", "RequestEnrollment",
"err", err,
"took", time.Since(begin),
)
}(time.Now())
return s.next.RequestEnrollment(ctx, req)
}
func (s loggingMiddleware) RequestConfig(ctx context.Context, req *pb.AgentApiRequest) (resp *pb.ConfigResponse, err error) {
defer func(begin time.Time) {
s.logger.Log(
"method", "RequestConfig",
"err", err,
"took", time.Since(begin),
)
}(time.Now())
return s.next.RequestConfig(ctx, req)
}
func (s loggingMiddleware) RequestQueries(ctx context.Context, req *pb.AgentApiRequest) (resp *pb.QueryCollection, err error) {
defer func(begin time.Time) {
s.logger.Log(
"method", "RequestQueries",
"err", err,
"took", time.Since(begin),
)
}(time.Now())
return s.next.RequestQueries(ctx, req)
}
func (s loggingMiddleware) PublishLogs(ctx context.Context, req *pb.LogCollection) (resp *pb.AgentApiResponse, err error) {
defer func(begin time.Time) {
s.logger.Log(
"method", "PublishLogs",
"err", err,
"took", time.Since(begin),
)
}(time.Now())
return s.next.PublishLogs(ctx, req)
}
func (s loggingMiddleware) PublishResults(ctx context.Context, req *pb.ResultCollection) (resp *pb.AgentApiResponse, err error) {
defer func(begin time.Time) {
s.logger.Log(
"method", "PublishResults",
"err", err,
"took", time.Since(begin),
)
}(time.Now())
return s.next.PublishResults(ctx, req)
}
func (s loggingMiddleware) CheckHealth(ctx context.Context, coll *pb.AgentApiRequest) (resp *pb.HealthCheckResponse, err error) {
defer func(begin time.Time) {
s.logger.Log(
"method", "CheckHealth",
"err", err,
"took", time.Since(begin),
)
}(time.Now())
return s.next.CheckHealth(ctx, coll)
}
+27 -14
View File
@@ -5,10 +5,12 @@ import (
"net/http"
"strings"
kitlog "github.com/go-kit/kit/log"
pb "github.com/kolide/agent-api"
"github.com/kolide/fleet/server/kolide"
"github.com/go-kit/kit/log"
launcher "github.com/kolide/launcher/service"
grpc "google.golang.org/grpc"
"github.com/kolide/fleet/server/health"
"github.com/kolide/fleet/server/kolide"
)
// Handler extends the grpc.Server, providing Handler that allows us to serve
@@ -17,25 +19,36 @@ type Handler struct {
*grpc.Server
}
// New creates a gRPC server to handler remote requests from launcher.
func New(svc kolide.OsqueryService, logger kitlog.Logger, opts ...grpc.ServerOption) *Handler {
binding := newAgentBinding(svc)
binding = newAuthMiddleware(svc)(binding)
binding = newLoggingMiddleware(logger)(binding)
server := grpc.NewServer(opts...)
pb.RegisterApiServer(server, binding)
return &Handler{server}
// New creates a gRPC server to handle remote requests from launcher.
func New(
tls kolide.OsqueryService,
logger log.Logger,
grpcServer *grpc.Server,
healthCheckers map[string]health.Checker,
) *Handler {
var svc launcher.KolideService
{
svc = &launcherWrapper{
tls: tls,
logger: logger,
healthCheckers: healthCheckers,
}
svc = launcher.LoggingMiddleware(logger)(svc)
}
endpoints := launcher.MakeServerEndpoints(svc)
server := launcher.NewGRPCServer(endpoints, logger)
launcher.RegisterGRPCServer(grpcServer, server)
return &Handler{grpcServer}
}
// Handler will route gRPC traffic to the gRPC server, other http traffic
// will be routed to normal http handler functions.
func (hgprc *Handler) Handler(h http.Handler) http.Handler {
func (hgprc *Handler) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
hgprc.ServeHTTP(w, r)
} else {
h.ServeHTTP(w, r)
next.ServeHTTP(w, r)
}
})
}
+3
View File
@@ -0,0 +1,3 @@
package mock
//go:generate mockimpl -o service_osquery.go "s *TLSService" "kolide.OsqueryService"
+83
View File
@@ -0,0 +1,83 @@
// Automatically generated by mockimpl. DO NOT EDIT!
package mock
import (
"context"
"github.com/kolide/fleet/server/kolide"
)
var _ kolide.OsqueryService = (*TLSService)(nil)
type EnrollAgentFunc func(ctx context.Context, enrollSecret string, hostIdentifier string) (nodeKey string, err error)
type AuthenticateHostFuncI func(ctx context.Context, nodeKey string) (host *kolide.Host, err error)
type GetClientConfigFunc func(ctx context.Context) (config *kolide.OsqueryConfig, err error)
type GetDistributedQueriesFunc func(ctx context.Context) (queries map[string]string, accelerate uint, err error)
type SubmitDistributedQueryResultsFunc func(ctx context.Context, results kolide.OsqueryDistributedQueryResults, statuses map[string]string) (err error)
type SubmitStatusLogsFunc func(ctx context.Context, logs []kolide.OsqueryStatusLog) (err error)
type SubmitResultLogsFunc func(ctx context.Context, logs []kolide.OsqueryResultLog) (err error)
type TLSService struct {
EnrollAgentFunc EnrollAgentFunc
EnrollAgentFuncInvoked bool
AuthenticateHostFunc AuthenticateHostFuncI
AuthenticateHostFuncInvoked bool
GetClientConfigFunc GetClientConfigFunc
GetClientConfigFuncInvoked bool
GetDistributedQueriesFunc GetDistributedQueriesFunc
GetDistributedQueriesFuncInvoked bool
SubmitDistributedQueryResultsFunc SubmitDistributedQueryResultsFunc
SubmitDistributedQueryResultsFuncInvoked bool
SubmitStatusLogsFunc SubmitStatusLogsFunc
SubmitStatusLogsFuncInvoked bool
SubmitResultLogsFunc SubmitResultLogsFunc
SubmitResultLogsFuncInvoked bool
}
func (s *TLSService) EnrollAgent(ctx context.Context, enrollSecret string, hostIdentifier string) (nodeKey string, err error) {
s.EnrollAgentFuncInvoked = true
return s.EnrollAgentFunc(ctx, enrollSecret, hostIdentifier)
}
func (s *TLSService) AuthenticateHost(ctx context.Context, nodeKey string) (host *kolide.Host, err error) {
s.AuthenticateHostFuncInvoked = true
return s.AuthenticateHostFunc(ctx, nodeKey)
}
func (s *TLSService) GetClientConfig(ctx context.Context) (config *kolide.OsqueryConfig, err error) {
s.GetClientConfigFuncInvoked = true
return s.GetClientConfigFunc(ctx)
}
func (s *TLSService) GetDistributedQueries(ctx context.Context) (queries map[string]string, accelerate uint, err error) {
s.GetDistributedQueriesFuncInvoked = true
return s.GetDistributedQueriesFunc(ctx)
}
func (s *TLSService) SubmitDistributedQueryResults(ctx context.Context, results kolide.OsqueryDistributedQueryResults, statuses map[string]string) (err error) {
s.SubmitDistributedQueryResultsFuncInvoked = true
return s.SubmitDistributedQueryResultsFunc(ctx, results, statuses)
}
func (s *TLSService) SubmitStatusLogs(ctx context.Context, logs []kolide.OsqueryStatusLog) (err error) {
s.SubmitStatusLogsFuncInvoked = true
return s.SubmitStatusLogsFunc(ctx, logs)
}
func (s *TLSService) SubmitResultLogs(ctx context.Context, logs []kolide.OsqueryResultLog) (err error) {
s.SubmitResultLogsFuncInvoked = true
return s.SubmitResultLogsFunc(ctx, logs)
}