From 25a8ccf40defdba989869cd5f210369f519ab014 Mon Sep 17 00:00:00 2001 From: zackattack01 Date: Mon, 22 Apr 2024 15:55:08 -0400 Subject: [PATCH 1/4] first pass at basic healthcheck writer --- cmd/launcher/launcher.go | 12 ++ cmd/launcher/uninstall_windows.go | 58 +++++++++- .../storage/sqlite/keyvalue_store_sqlite.go | 8 +- .../000002_add_health_check_table.down.sqlite | 1 + .../000002_add_health_check_table.up.sqlite | 4 + ee/agent/storage/sqlite/sql_store_sqlite.go | 78 +++++++++++++ ee/agent/types/keyvalue_store.go | 6 +- ee/agent/types/sql_store.go | 24 ++++ ee/debug/checkups/checkups.go | 1 + ee/debug/checkups/healthcheck.go | 105 ++++++++++++++++++ ee/restartservice/health_check_reader.go | 36 ++++++ ee/restartservice/health_check_writer.go | 52 +++++++++ 12 files changed, 380 insertions(+), 5 deletions(-) create mode 100644 ee/agent/storage/sqlite/migrations/000002_add_health_check_table.down.sqlite create mode 100644 ee/agent/storage/sqlite/migrations/000002_add_health_check_table.up.sqlite create mode 100644 ee/agent/storage/sqlite/sql_store_sqlite.go create mode 100644 ee/agent/types/sql_store.go create mode 100644 ee/debug/checkups/healthcheck.go create mode 100644 ee/restartservice/health_check_reader.go create mode 100644 ee/restartservice/health_check_writer.go diff --git a/cmd/launcher/launcher.go b/cmd/launcher/launcher.go index 93d46309d..3c6ac48ff 100644 --- a/cmd/launcher/launcher.go +++ b/cmd/launcher/launcher.go @@ -40,6 +40,7 @@ import ( "github.com/kolide/launcher/ee/localserver" kolidelog "github.com/kolide/launcher/ee/log/osquerylogs" "github.com/kolide/launcher/ee/powereventwatcher" + "github.com/kolide/launcher/ee/restartservice" "github.com/kolide/launcher/ee/tuf" "github.com/kolide/launcher/pkg/augeas" "github.com/kolide/launcher/pkg/backoff" @@ -271,6 +272,17 @@ func runLauncher(ctx context.Context, cancel func(), multiSlogger, systemMultiSl go checkpointer.Once(ctx) runGroup.Add("logcheckpoint", checkpointer.Run, checkpointer.Interrupt) + healthCheckStore, err := restartservice.OpenWriter(ctx, k.RootDirectory()) + if err != nil { // log an error but don't stop runLauncher from continuing + slogger.Log(ctx, slog.LevelError, + "could not init health check result writer store, history will be absent for this run", + "err", err, + ) + } else { + healthchecker := checkups.NewHealthChecker(slogger, k, healthCheckStore) + runGroup.Add("healthchecker", healthchecker.Run, healthchecker.Interrupt) + } + // Create a channel for signals sigChannel := make(chan os.Signal, 1) diff --git a/cmd/launcher/uninstall_windows.go b/cmd/launcher/uninstall_windows.go index ac83fb4fb..eb4ca6a2a 100644 --- a/cmd/launcher/uninstall_windows.go +++ b/cmd/launcher/uninstall_windows.go @@ -5,10 +5,62 @@ package main import ( "context" - "errors" + "fmt" + "time" + + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" ) -func removeLauncher(ctx context.Context, identifier string) error { +func stopService(service *mgr.Service) error { + status, err := service.Control(svc.Stop) + if err != nil { + return fmt.Errorf("stopping %s service: %w", service.Name, err) + } + + timeout := time.Now().Add(10 * time.Second) + for status.State != svc.Stopped { + if timeout.Before(time.Now()) { + return fmt.Errorf("timeout waiting for %s service to stop", service.Name) + } + + time.Sleep(500 * time.Millisecond) + status, err = service.Query() + if err != nil { + return fmt.Errorf("could not retrieve service status: %w", err) + } + } + + return nil +} + +func removeService(service *mgr.Service) error { + if err := stopService(service); err != nil { + return err + } + + return service.Delete() +} + +func removeLauncher(_ context.Context, _ string) error { // Uninstall is not implemented for Windows - users have to use add/remove programs themselves - return errors.New("Uninstall subcommand is not supported for Windows platforms.") + // return errors.New("Uninstall subcommand is not supported for Windows platforms.") + + sman, err := mgr.Connect() + if err != nil { + return fmt.Errorf("connecting to service control manager: %w", err) + } + + defer sman.Disconnect() + + restartService, err := sman.OpenService(launcherRestartServiceName) + // would be better to check the individual error here but there will be one if the service does + // not exist, in which case it's fine to just skip this anyway + if err != nil { + return err + } + + defer restartService.Close() + + return removeService(restartService) } diff --git a/ee/agent/storage/sqlite/keyvalue_store_sqlite.go b/ee/agent/storage/sqlite/keyvalue_store_sqlite.go index c63056047..a8db2fa7c 100644 --- a/ee/agent/storage/sqlite/keyvalue_store_sqlite.go +++ b/ee/agent/storage/sqlite/keyvalue_store_sqlite.go @@ -21,7 +21,9 @@ import ( type storeName int const ( - StartupSettingsStore storeName = iota + StartupSettingsStore storeName = iota + HealthCheckStore storeName = 1 + RestartServiceLogStore storeName = 2 ) // String translates the exported int constant to the actual name of the @@ -30,6 +32,10 @@ func (s storeName) String() string { switch s { case StartupSettingsStore: return "startup_settings" + case HealthCheckStore: + return "health_check" + case RestartServiceLogStore: + return "restart_service_logs" } return "" diff --git a/ee/agent/storage/sqlite/migrations/000002_add_health_check_table.down.sqlite b/ee/agent/storage/sqlite/migrations/000002_add_health_check_table.down.sqlite new file mode 100644 index 000000000..e93abb8ae --- /dev/null +++ b/ee/agent/storage/sqlite/migrations/000002_add_health_check_table.down.sqlite @@ -0,0 +1 @@ +DROP TABLE IF EXISTS health_check_results; \ No newline at end of file diff --git a/ee/agent/storage/sqlite/migrations/000002_add_health_check_table.up.sqlite b/ee/agent/storage/sqlite/migrations/000002_add_health_check_table.up.sqlite new file mode 100644 index 000000000..5b4d0cac4 --- /dev/null +++ b/ee/agent/storage/sqlite/migrations/000002_add_health_check_table.up.sqlite @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS health_check_results ( + timestamp INT NOT NULL, + values TEXT +); \ No newline at end of file diff --git a/ee/agent/storage/sqlite/sql_store_sqlite.go b/ee/agent/storage/sqlite/sql_store_sqlite.go new file mode 100644 index 000000000..845c8070b --- /dev/null +++ b/ee/agent/storage/sqlite/sql_store_sqlite.go @@ -0,0 +1,78 @@ +package agentsqlite + +import ( + "context" + "database/sql" + "errors" + "fmt" + + _ "modernc.org/sqlite" +) + +func (s *sqliteStore) FetchResults(ctx context.Context, columnName string) ([][]byte, error) { + results := make([][]byte, 0) + + if s == nil || s.conn == nil { + return results, errors.New("store is nil") + } + + // It's fine to interpolate the table name into the query because we allowlist via `storeName` type + query := fmt.Sprintf(`SELECT timestamp, ? FROM %s;`, s.tableName) + rows, err := s.conn.QueryContext(ctx, query, columnName) + if err != nil { + return results, err + } + + defer rows.Close() + + for rows.Next() { + var timestamp int64 + var result string + if err := rows.Scan(×tamp, &result); err != nil { + return results, err + } + results = append(results, []byte(result)) + } + + return results, nil +} + +func (s *sqliteStore) FetchLatestResult(ctx context.Context, columnName string) ([]byte, error) { + if s == nil || s.conn == nil { + return []byte{}, errors.New("store is nil") + } + + // It's fine to interpolate the table name into the query because we allowlist via `storeName` type + query := fmt.Sprintf(`SELECT timestamp, ? FROM %s ORDER BY timestamp DESC LIMIT 1;`, s.tableName) + var timestamp int64 + var result string + + err := s.conn.QueryRowContext(ctx, query, columnName).Scan(×tamp, &result) + switch { + case err == sql.ErrNoRows: + return []byte{}, nil + case err != nil: + return []byte{}, err + default: + return []byte(result), nil + } +} + +func (s *sqliteStore) AddResult(ctx context.Context, columnName string, timestamp int64, result []byte) error { + if s == nil || s.conn == nil { + return errors.New("store is nil") + } + + if s.readOnly { + return errors.New("cannot perform AddResult with RO connection") + } + + // It's fine to interpolate the table name into the query because we allowlist via `storeName` type + insertSql := fmt.Sprintf(`INSERT INTO %s (timestamp, ?) VALUES (?, ?);`, s.tableName) + + if _, err := s.conn.Exec(insertSql, columnName, timestamp, string(result)); err != nil { + return fmt.Errorf("inserting into %s: %w", s.tableName, err) + } + + return nil +} \ No newline at end of file diff --git a/ee/agent/types/keyvalue_store.go b/ee/agent/types/keyvalue_store.go index d8c9c10f8..7cdda32c1 100644 --- a/ee/agent/types/keyvalue_store.go +++ b/ee/agent/types/keyvalue_store.go @@ -61,10 +61,14 @@ type GetterSetter interface { Setter } +type Closer interface { + Close() error +} + // GetterCloser extends the Getter interface with a Close method. type GetterCloser interface { Getter - Close() error + Closer } // GetterUpdaterCloser groups the Get, Update, and Close methods. diff --git a/ee/agent/types/sql_store.go b/ee/agent/types/sql_store.go new file mode 100644 index 000000000..108696c0e --- /dev/null +++ b/ee/agent/types/sql_store.go @@ -0,0 +1,24 @@ +package types + +import ( + "context" +) + +// ResultFetcher is an interface for querying a single text field from a structured data store. +// This is intentionally vague for potential future re-use, allowing the caller to unmarshal string results as needed. +// This interface is compatible with any tables that include a timestamp, and any text field of interest +type ResultFetcher interface { + // Fetch retrieves all rows provided by the results of executing query + FetchResults(ctx context.Context, columnName string) ([][]byte, error) + // FetchLatest retrieves the most recent value for columnName + FetchLatestResult(ctx context.Context, columnName string) ([]byte, error) + Closer +} + +type ResultSetter interface { + // AddResult marshals + AddResult(ctx context.Context, columnName string, timestamp int64, result []byte) error + Closer +} + +// TODO add rotation interface to cap limit on health check results diff --git a/ee/debug/checkups/checkups.go b/ee/debug/checkups/checkups.go index fb54f1769..8d138f8ff 100644 --- a/ee/debug/checkups/checkups.go +++ b/ee/debug/checkups/checkups.go @@ -80,6 +80,7 @@ const ( doctorSupported targetBits = 1 << iota flareSupported logSupported + healthCheckSupported ) //const checkupFor iota diff --git a/ee/debug/checkups/healthcheck.go b/ee/debug/checkups/healthcheck.go new file mode 100644 index 000000000..ca75d315d --- /dev/null +++ b/ee/debug/checkups/healthcheck.go @@ -0,0 +1,105 @@ +package checkups + +import ( + "context" + "encoding/json" + "io" + "log/slog" + "strings" + "time" + + "github.com/kolide/launcher/ee/agent/types" + "github.com/kolide/launcher/ee/restartservice" +) + +type ( + healthChecker struct { + slogger *slog.Logger + knapsack types.Knapsack + interrupt chan struct{} + interrupted bool + writer *restartservice.HealthCheckWriter + } +) + +func NewHealthChecker(slogger *slog.Logger, k types.Knapsack, writer *restartservice.HealthCheckWriter) *healthChecker { + return &healthChecker{ + slogger: slogger.With("component", "healthchecker"), + knapsack: k, + interrupt: make(chan struct{}, 1), + writer: writer, + } +} + +// Run starts a healthchecker routine. The purpose of this is to +// maintain a historical record of launcher health for general debugging +// and for our watchdog service to observe unhealthy states and respond accordingly +func (c *healthChecker) Run() error { + ticker := time.NewTicker(time.Minute * 1) + defer ticker.Stop() + + for { + c.Once(context.TODO()) + + select { + case <-ticker.C: + continue + case <-c.interrupt: + c.slogger.Log(context.TODO(), slog.LevelDebug, + "interrupt received, exiting execute loop", + ) + return nil + } + } +} + +func (c *healthChecker) Interrupt(_ error) { + // Only perform shutdown tasks on first call to interrupt -- no need to repeat on potential extra calls. + if c.interrupted { + return + } + + c.interrupted = true + + c.interrupt <- struct{}{} +} + +func (c *healthChecker) Once(ctx context.Context) { + checkups := checkupsFor(c.knapsack, healthCheckSupported) + results := make(map[string]map[string]any) + checkupTime := time.Now().Unix() + + for _, checkup := range checkups { + checkup.Run(ctx, io.Discard) + checkupName := normalizeCheckupName(checkup.Name()) + results[checkupName] = make(map[string]any) + results[checkupName]["status"] = checkup.Status() + results[checkupName]["data"] = checkup.Data() + } + + resultsJson, err := json.Marshal(results) + if err != nil { + c.slogger.Log(ctx, slog.LevelWarn, + "failure encoding health check results", + "err", err, + ) + + return + } + + if err = c.writer.AddHealthCheckResult(ctx, checkupTime, resultsJson); err != nil { + c.slogger.Log(ctx, slog.LevelWarn, + "failure writing out health check results", + "err", err, + ) + + return + } +} + +func normalizeCheckupName(name string) string { + return strings.ReplaceAll( + strings.ToLower(name), + " ", "_", + ) +} diff --git a/ee/restartservice/health_check_reader.go b/ee/restartservice/health_check_reader.go new file mode 100644 index 000000000..000e0bbee --- /dev/null +++ b/ee/restartservice/health_check_reader.go @@ -0,0 +1,36 @@ +package restartservice + +import ( + "context" + "fmt" + "time" + + agentsqlite "github.com/kolide/launcher/ee/agent/storage/sqlite" + "github.com/kolide/launcher/ee/agent/types" +) + +type ( + HealthCheckResult struct { + timestamp time.Time + healthy bool + errors []string + } + + healthCheckReader struct { + store types.ResultFetcher + } +) + +func OpenReader(ctx context.Context, rootDirectory string) (*healthCheckReader, error) { + store, err := agentsqlite.OpenRO(ctx, rootDirectory, agentsqlite.HealthCheckStore) + if err != nil { + return nil, fmt.Errorf("opening healthcheck db in %s: %w", rootDirectory, err) + } + + return &healthCheckReader{store: store}, nil +} + + +func (r *healthCheckReader) Close() error { + return r.store.Close() +} diff --git a/ee/restartservice/health_check_writer.go b/ee/restartservice/health_check_writer.go new file mode 100644 index 000000000..a5ed97a3f --- /dev/null +++ b/ee/restartservice/health_check_writer.go @@ -0,0 +1,52 @@ +package restartservice + +import ( + "context" + "errors" + "fmt" + + agentsqlite "github.com/kolide/launcher/ee/agent/storage/sqlite" + "github.com/kolide/launcher/ee/agent/types" + "github.com/kolide/launcher/pkg/traces" +) + +// HealthCheckWriter adheres to the ResultSetter interface +type HealthCheckWriter struct { + store types.ResultSetter +} + +const healthCheckResultsColumn string = "values" + +// OpenWriter returns a new health check results writer, creating and initializing +// the database if necessary. +func OpenWriter(ctx context.Context, rootDirectory string) (*HealthCheckWriter, error) { + ctx, span := traces.StartSpan(ctx) + defer span.End() + + store, err := agentsqlite.OpenRW(ctx, rootDirectory, agentsqlite.HealthCheckStore) + if err != nil { + return nil, fmt.Errorf("opening healthcheck db in %s: %w", rootDirectory, err) + } + + s := &HealthCheckWriter{ + store: store, + } + + return s, nil +} + +func (hw *HealthCheckWriter) AddHealthCheckResult(ctx context.Context, timestamp int64, value []byte) error { + if hw == nil || hw.store == nil { + return errors.New("store is nil") + } + + if err := hw.store.AddResult(ctx, healthCheckResultsColumn, timestamp, value); err != nil { + return fmt.Errorf("adding healthcheck result: %w", err) + } + + return nil +} + +func (r *HealthCheckWriter) Close() error { + return r.store.Close() +} From 08180edc2153ce1dc879854c817918183103a857 Mon Sep 17 00:00:00 2001 From: zackattack01 Date: Mon, 22 Apr 2024 17:03:55 -0400 Subject: [PATCH 2/4] health check writer working with one sample checkup --- ee/agent/storage/sqlite/keyvalue_store_sqlite.go | 2 +- .../000002_add_health_check_table.up.sqlite | 2 +- ee/agent/storage/sqlite/sql_store_sqlite.go | 6 +++--- ee/agent/types/sql_store.go | 2 +- ee/debug/checkups/checkups.go | 5 ++++- ee/debug/checkups/healthcheck.go | 16 +++++++++++----- ee/restartservice/health_check_writer.go | 4 +--- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/ee/agent/storage/sqlite/keyvalue_store_sqlite.go b/ee/agent/storage/sqlite/keyvalue_store_sqlite.go index a8db2fa7c..18a14a431 100644 --- a/ee/agent/storage/sqlite/keyvalue_store_sqlite.go +++ b/ee/agent/storage/sqlite/keyvalue_store_sqlite.go @@ -33,7 +33,7 @@ func (s storeName) String() string { case StartupSettingsStore: return "startup_settings" case HealthCheckStore: - return "health_check" + return "health_check_results" case RestartServiceLogStore: return "restart_service_logs" } diff --git a/ee/agent/storage/sqlite/migrations/000002_add_health_check_table.up.sqlite b/ee/agent/storage/sqlite/migrations/000002_add_health_check_table.up.sqlite index 5b4d0cac4..d0218a1a9 100644 --- a/ee/agent/storage/sqlite/migrations/000002_add_health_check_table.up.sqlite +++ b/ee/agent/storage/sqlite/migrations/000002_add_health_check_table.up.sqlite @@ -1,4 +1,4 @@ CREATE TABLE IF NOT EXISTS health_check_results ( timestamp INT NOT NULL, - values TEXT + results TEXT ); \ No newline at end of file diff --git a/ee/agent/storage/sqlite/sql_store_sqlite.go b/ee/agent/storage/sqlite/sql_store_sqlite.go index 845c8070b..6b08ee353 100644 --- a/ee/agent/storage/sqlite/sql_store_sqlite.go +++ b/ee/agent/storage/sqlite/sql_store_sqlite.go @@ -58,7 +58,7 @@ func (s *sqliteStore) FetchLatestResult(ctx context.Context, columnName string) } } -func (s *sqliteStore) AddResult(ctx context.Context, columnName string, timestamp int64, result []byte) error { +func (s *sqliteStore) AddResult(ctx context.Context, timestamp int64, result []byte) error { if s == nil || s.conn == nil { return errors.New("store is nil") } @@ -68,9 +68,9 @@ func (s *sqliteStore) AddResult(ctx context.Context, columnName string, timestam } // It's fine to interpolate the table name into the query because we allowlist via `storeName` type - insertSql := fmt.Sprintf(`INSERT INTO %s (timestamp, ?) VALUES (?, ?);`, s.tableName) + insertSql := fmt.Sprintf(`INSERT INTO %s (timestamp, results) VALUES (?, ?);`, s.tableName) - if _, err := s.conn.Exec(insertSql, columnName, timestamp, string(result)); err != nil { + if _, err := s.conn.Exec(insertSql, timestamp, string(result)); err != nil { return fmt.Errorf("inserting into %s: %w", s.tableName, err) } diff --git a/ee/agent/types/sql_store.go b/ee/agent/types/sql_store.go index 108696c0e..54abbad55 100644 --- a/ee/agent/types/sql_store.go +++ b/ee/agent/types/sql_store.go @@ -17,7 +17,7 @@ type ResultFetcher interface { type ResultSetter interface { // AddResult marshals - AddResult(ctx context.Context, columnName string, timestamp int64, result []byte) error + AddResult(ctx context.Context, timestamp int64, result []byte) error Closer } diff --git a/ee/debug/checkups/checkups.go b/ee/debug/checkups/checkups.go index 8d138f8ff..0e63d6f0c 100644 --- a/ee/debug/checkups/checkups.go +++ b/ee/debug/checkups/checkups.go @@ -80,6 +80,9 @@ const ( doctorSupported targetBits = 1 << iota flareSupported logSupported + // note that in the future a failing checkup that is healthCheckSupported should + // result in a launcher restart from our watchdog service. ensure that this makes + // sense for a checkup before adding the healthCheckSupported bits healthCheckSupported ) @@ -95,7 +98,7 @@ func checkupsFor(k types.Knapsack, target targetBits) []checkupInt { {&Platform{}, doctorSupported | flareSupported | logSupported}, {&Version{k: k}, doctorSupported | flareSupported | logSupported}, {&hostInfoCheckup{k: k}, doctorSupported | flareSupported | logSupported}, - {&Processes{}, doctorSupported | flareSupported}, + {&Processes{}, doctorSupported | flareSupported | healthCheckSupported}, {&RootDirectory{k: k}, doctorSupported | flareSupported}, {&Connectivity{k: k}, doctorSupported | flareSupported | logSupported}, {&Logs{k: k}, doctorSupported | flareSupported}, diff --git a/ee/debug/checkups/healthcheck.go b/ee/debug/checkups/healthcheck.go index ca75d315d..622582dd9 100644 --- a/ee/debug/checkups/healthcheck.go +++ b/ee/debug/checkups/healthcheck.go @@ -35,7 +35,7 @@ func NewHealthChecker(slogger *slog.Logger, k types.Knapsack, writer *restartser // maintain a historical record of launcher health for general debugging // and for our watchdog service to observe unhealthy states and respond accordingly func (c *healthChecker) Run() error { - ticker := time.NewTicker(time.Minute * 1) + ticker := time.NewTicker(time.Minute * 30) defer ticker.Stop() for { @@ -66,15 +66,21 @@ func (c *healthChecker) Interrupt(_ error) { func (c *healthChecker) Once(ctx context.Context) { checkups := checkupsFor(c.knapsack, healthCheckSupported) - results := make(map[string]map[string]any) + results := make(map[string]Status) checkupTime := time.Now().Unix() for _, checkup := range checkups { checkup.Run(ctx, io.Discard) checkupName := normalizeCheckupName(checkup.Name()) - results[checkupName] = make(map[string]any) - results[checkupName]["status"] = checkup.Status() - results[checkupName]["data"] = checkup.Data() + results[checkupName] = checkup.Status() + // log all data for debugging if Failing + if checkup.Status() == Failing { + c.slogger.Log(ctx, slog.LevelWarn, + "detected health check failure", + "checkup", checkupName, + "data", checkup.Data(), + ) + } } resultsJson, err := json.Marshal(results) diff --git a/ee/restartservice/health_check_writer.go b/ee/restartservice/health_check_writer.go index a5ed97a3f..066bc084e 100644 --- a/ee/restartservice/health_check_writer.go +++ b/ee/restartservice/health_check_writer.go @@ -15,8 +15,6 @@ type HealthCheckWriter struct { store types.ResultSetter } -const healthCheckResultsColumn string = "values" - // OpenWriter returns a new health check results writer, creating and initializing // the database if necessary. func OpenWriter(ctx context.Context, rootDirectory string) (*HealthCheckWriter, error) { @@ -40,7 +38,7 @@ func (hw *HealthCheckWriter) AddHealthCheckResult(ctx context.Context, timestamp return errors.New("store is nil") } - if err := hw.store.AddResult(ctx, healthCheckResultsColumn, timestamp, value); err != nil { + if err := hw.store.AddResult(ctx, timestamp, value); err != nil { return fmt.Errorf("adding healthcheck result: %w", err) } From 6c338be4a9a8269aa91a42b03dde4b030eb50703 Mon Sep 17 00:00:00 2001 From: zackattack01 Date: Tue, 23 Apr 2024 11:46:34 -0400 Subject: [PATCH 3/4] dont allow column name selection for resultfetcher interface --- ee/agent/storage/sqlite/sql_store_sqlite.go | 14 +++++++------- ee/agent/types/sql_store.go | 12 ++++++------ ee/restartservice/health_check_reader.go | 8 -------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/ee/agent/storage/sqlite/sql_store_sqlite.go b/ee/agent/storage/sqlite/sql_store_sqlite.go index 6b08ee353..21fd7d0c2 100644 --- a/ee/agent/storage/sqlite/sql_store_sqlite.go +++ b/ee/agent/storage/sqlite/sql_store_sqlite.go @@ -9,7 +9,7 @@ import ( _ "modernc.org/sqlite" ) -func (s *sqliteStore) FetchResults(ctx context.Context, columnName string) ([][]byte, error) { +func (s *sqliteStore) FetchResults(ctx context.Context) ([][]byte, error) { results := make([][]byte, 0) if s == nil || s.conn == nil { @@ -17,8 +17,8 @@ func (s *sqliteStore) FetchResults(ctx context.Context, columnName string) ([][] } // It's fine to interpolate the table name into the query because we allowlist via `storeName` type - query := fmt.Sprintf(`SELECT timestamp, ? FROM %s;`, s.tableName) - rows, err := s.conn.QueryContext(ctx, query, columnName) + query := fmt.Sprintf(`SELECT timestamp, results FROM %s;`, s.tableName) + rows, err := s.conn.QueryContext(ctx, query) if err != nil { return results, err } @@ -37,17 +37,17 @@ func (s *sqliteStore) FetchResults(ctx context.Context, columnName string) ([][] return results, nil } -func (s *sqliteStore) FetchLatestResult(ctx context.Context, columnName string) ([]byte, error) { +func (s *sqliteStore) FetchLatestResult(ctx context.Context) ([]byte, error) { if s == nil || s.conn == nil { return []byte{}, errors.New("store is nil") } // It's fine to interpolate the table name into the query because we allowlist via `storeName` type - query := fmt.Sprintf(`SELECT timestamp, ? FROM %s ORDER BY timestamp DESC LIMIT 1;`, s.tableName) + query := fmt.Sprintf(`SELECT timestamp, results FROM %s ORDER BY timestamp DESC LIMIT 1;`, s.tableName) var timestamp int64 var result string - err := s.conn.QueryRowContext(ctx, query, columnName).Scan(×tamp, &result) + err := s.conn.QueryRowContext(ctx, query).Scan(×tamp, &result) switch { case err == sql.ErrNoRows: return []byte{}, nil @@ -75,4 +75,4 @@ func (s *sqliteStore) AddResult(ctx context.Context, timestamp int64, result []b } return nil -} \ No newline at end of file +} diff --git a/ee/agent/types/sql_store.go b/ee/agent/types/sql_store.go index 54abbad55..ab5407934 100644 --- a/ee/agent/types/sql_store.go +++ b/ee/agent/types/sql_store.go @@ -6,17 +6,17 @@ import ( // ResultFetcher is an interface for querying a single text field from a structured data store. // This is intentionally vague for potential future re-use, allowing the caller to unmarshal string results as needed. -// This interface is compatible with any tables that include a timestamp, and any text field of interest +// This was initially intended to support the sqlite health_check_results table type ResultFetcher interface { - // Fetch retrieves all rows provided by the results of executing query - FetchResults(ctx context.Context, columnName string) ([][]byte, error) - // FetchLatest retrieves the most recent value for columnName - FetchLatestResult(ctx context.Context, columnName string) ([]byte, error) + // Fetch retrieves all results rows + FetchResults(ctx context.Context) ([][]byte, error) + // FetchLatest retrieves the most recent result based on timestamp column + FetchLatestResult(ctx context.Context) ([]byte, error) Closer } type ResultSetter interface { - // AddResult marshals + // AddResult persists a marshalled result entry alongside the provided unix timestamp AddResult(ctx context.Context, timestamp int64, result []byte) error Closer } diff --git a/ee/restartservice/health_check_reader.go b/ee/restartservice/health_check_reader.go index 000e0bbee..ba259170a 100644 --- a/ee/restartservice/health_check_reader.go +++ b/ee/restartservice/health_check_reader.go @@ -3,19 +3,12 @@ package restartservice import ( "context" "fmt" - "time" agentsqlite "github.com/kolide/launcher/ee/agent/storage/sqlite" "github.com/kolide/launcher/ee/agent/types" ) type ( - HealthCheckResult struct { - timestamp time.Time - healthy bool - errors []string - } - healthCheckReader struct { store types.ResultFetcher } @@ -30,7 +23,6 @@ func OpenReader(ctx context.Context, rootDirectory string) (*healthCheckReader, return &healthCheckReader{store: store}, nil } - func (r *healthCheckReader) Close() error { return r.store.Close() } From f1ee79ce50bd953aadde4a377ac5fd1749d08f2b Mon Sep 17 00:00:00 2001 From: zackattack01 Date: Tue, 23 Apr 2024 12:04:20 -0400 Subject: [PATCH 4/4] unstage changes to windows uninstall --- cmd/launcher/uninstall_windows.go | 58 ++----------------------------- 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/cmd/launcher/uninstall_windows.go b/cmd/launcher/uninstall_windows.go index eb4ca6a2a..ac83fb4fb 100644 --- a/cmd/launcher/uninstall_windows.go +++ b/cmd/launcher/uninstall_windows.go @@ -5,62 +5,10 @@ package main import ( "context" - "fmt" - "time" - - "golang.org/x/sys/windows/svc" - "golang.org/x/sys/windows/svc/mgr" + "errors" ) -func stopService(service *mgr.Service) error { - status, err := service.Control(svc.Stop) - if err != nil { - return fmt.Errorf("stopping %s service: %w", service.Name, err) - } - - timeout := time.Now().Add(10 * time.Second) - for status.State != svc.Stopped { - if timeout.Before(time.Now()) { - return fmt.Errorf("timeout waiting for %s service to stop", service.Name) - } - - time.Sleep(500 * time.Millisecond) - status, err = service.Query() - if err != nil { - return fmt.Errorf("could not retrieve service status: %w", err) - } - } - - return nil -} - -func removeService(service *mgr.Service) error { - if err := stopService(service); err != nil { - return err - } - - return service.Delete() -} - -func removeLauncher(_ context.Context, _ string) error { +func removeLauncher(ctx context.Context, identifier string) error { // Uninstall is not implemented for Windows - users have to use add/remove programs themselves - // return errors.New("Uninstall subcommand is not supported for Windows platforms.") - - sman, err := mgr.Connect() - if err != nil { - return fmt.Errorf("connecting to service control manager: %w", err) - } - - defer sman.Disconnect() - - restartService, err := sman.OpenService(launcherRestartServiceName) - // would be better to check the individual error here but there will be one if the service does - // not exist, in which case it's fine to just skip this anyway - if err != nil { - return err - } - - defer restartService.Close() - - return removeService(restartService) + return errors.New("Uninstall subcommand is not supported for Windows platforms.") }