Skip to content

Commit

Permalink
launcher doctor subcommand (#1197)
Browse files Browse the repository at this point in the history
  • Loading branch information
CJ Barrett authored May 26, 2023
1 parent 50e10cb commit 7a926d0
Show file tree
Hide file tree
Showing 17 changed files with 885 additions and 44 deletions.
499 changes: 499 additions & 0 deletions cmd/launcher/doctor.go

Large diffs are not rendered by default.

198 changes: 198 additions & 0 deletions cmd/launcher/doctor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package main

import (
"errors"
"io"
"runtime"
"testing"

"github.com/kolide/kit/version"
"github.com/kolide/launcher/pkg/autoupdate"
"github.com/stretchr/testify/require"
)

func TestMain(m *testing.M) {
// We don't care about the actual CLI output
doctorWriter = io.Discard
}

func TestRunCheckups(t *testing.T) {
t.Parallel()

tests := []struct {
name string
checkups []*checkup
}{
{
name: "successful checkups",
checkups: []*checkup{
{
name: "do nothing",
check: func() (string, error) {
return "", nil
},
},
},
},
{
name: "failed checkup",
checkups: []*checkup{
{
name: "do nothing",
check: func() (string, error) {
return "", nil
},
},
{
name: "return error",
check: func() (string, error) {
return "", errors.New("checkup error")
},
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

runCheckups(tt.checkups)
})
}
}

func TestCheckupPlatform(t *testing.T) {
t.Parallel()

tests := []struct {
name string
os string
expectedErr bool
}{
{
name: "supported",
os: runtime.GOOS,
expectedErr: false,
},
{
name: "unsupported",
os: "not-an-os",
expectedErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

_, err := checkupPlatform(tt.os)
if tt.expectedErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

func TestCheckupArch(t *testing.T) {
t.Parallel()

tests := []struct {
name string
os string
expectedErr bool
}{
{
name: "supported",
os: runtime.GOARCH,
expectedErr: false,
},
{
name: "unsupported",
os: "not-an-arch",
expectedErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

_, err := checkupArch(tt.os)
if tt.expectedErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

func TestCheckupRootDir(t *testing.T) {
t.Parallel()

tests := []struct {
name string
filepaths []string
expectedErr bool
}{
{
name: "present",
filepaths: []string{"debug.json", "launcher.db", "osquery.db"},
expectedErr: false,
},
{
name: "not present",
filepaths: []string{"not-an-important-file"},
expectedErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

_, err := checkupRootDir(tt.filepaths)
if tt.expectedErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

func TestCheckupVersion(t *testing.T) {
t.Parallel()

tests := []struct {
name string
updateChannel string
tufServerURL string
version version.Info
expectedErr bool
}{
{
name: "happy path",
updateChannel: autoupdate.Stable.String(),
tufServerURL: "https://tuf.kolide.com",
version: version.Version(),
expectedErr: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

_, err := checkupVersion(tt.updateChannel, tt.tufServerURL, tt.version)
if tt.expectedErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
34 changes: 28 additions & 6 deletions cmd/launcher/flare.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,26 @@ import (
"github.com/kolide/kit/ulid"
"github.com/kolide/kit/version"
"github.com/kolide/launcher/pkg/agent"
"github.com/kolide/launcher/pkg/agent/flags"
"github.com/kolide/launcher/pkg/agent/knapsack"
"github.com/kolide/launcher/pkg/autoupdate"
"github.com/kolide/launcher/pkg/osquery/runtime"
"github.com/kolide/launcher/pkg/service"
osquerygo "github.com/osquery/osquery-go"
)

func runFlare(args []string) error {
flagset := flag.NewFlagSet("launcher flare", flag.ExitOnError)
// Flare assumes a launcher installation (at least partially) exists
// Overriding some of the default values allows options to be parsed making this assumption
defaultKolideHosted = true
defaultAutoupdate = true
setDefaultPaths()

opts, err := parseOptions("flare", args)
if err != nil {
return err
}

var (
flHostname = flag.String("hostname", "dababe.launcher.kolide.com:443", "")

Expand All @@ -39,19 +51,19 @@ func runFlare(args []string) error {
insecureTLS = env.Bool("KOLIDE_LAUNCHER_INSECURE", false)
insecureTransport = env.Bool("KOLIDE_LAUNCHER_INSECURE_TRANSPORT", false)
flareSocketPath = env.String("FLARE_SOCKET_PATH", agent.TempPath("flare.sock"))
tarDirPath = env.String("KOLIDE_LAUNCHER_FLARE_TAR_DIR_PATH", "")

certPins [][]byte
rootPool *x509.CertPool
)
flagset.Usage = commandUsage(flagset, "launcher flare")
if err := flagset.Parse(args); err != nil {
return err
}

id := ulid.New()
b := new(bytes.Buffer)
reportName := fmt.Sprintf("kolide_launcher_flare_report_%s", id)
tarOut, err := os.Create(fmt.Sprintf("%s.tar.gz", reportName))
reportPath := fmt.Sprintf("%s.tar.gz", filepath.Join(tarDirPath, reportName))
output(b, stdout, fmt.Sprintf("Generating flare report file: %s\n", reportPath))

tarOut, err := os.Create(reportPath)
if err != nil {
fatal(b, err)
}
Expand Down Expand Up @@ -111,6 +123,16 @@ func runFlare(args []string) error {
output(b, stdout, "%v\n", string(jsonVersion))

logger := log.NewLogfmtLogger(b)
fcOpts := []flags.Option{flags.WithCmdLineOpts(opts)}
flagController := flags.NewFlagController(logger, nil, fcOpts...)
k := knapsack.New(nil, flagController, nil)

output(b, stdout, "\nStarting Launcher Doctor\n")
// Run doctor but disable color output since this is being directed to a file
os.Setenv("NO_COLOR", "1")
buildAndRunCheckups(logger, k, opts, b)
output(b, stdout, "\nEnd of Launcher Doctor\n")

err = reportGRPCNetwork(
logger,
serverURL,
Expand Down
2 changes: 1 addition & 1 deletion cmd/launcher/interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func runInteractive(args []string) error {

flagset.Var(&flOsqueryFlags, "osquery_flag", "Flags to pass to osquery (possibly overriding Launcher defaults)")

flagset.Usage = commandUsage(flagset, "interactive")
flagset.Usage = commandUsage(flagset, "launcher interactive")
if err := flagset.Parse(args); err != nil {
return err
}
Expand Down
4 changes: 3 additions & 1 deletion cmd/launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func main() {
os.Exit(0)
}

opts, err := parseOptions(os.Args[1:])
opts, err := parseOptions("", os.Args[1:])
if err != nil {
level.Info(logger).Log("err", err)
os.Exit(1)
Expand Down Expand Up @@ -121,6 +121,8 @@ func runSubcommands() error {
run = runSocket
case "query":
run = runQuery
case "doctor":
run = runDoctor
case "flare":
run = runFlare
case "svc":
Expand Down
32 changes: 26 additions & 6 deletions cmd/launcher/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ const (
skipEnvParse = runtime.GOOS == "windows" // skip environmental variable parsing on windows
)

var (
// When launcher proper runs, it's expected that these defaults are their zero values
// However, special launcher subcommands such as launcher doctor can override these
defaultRootDirectoryPath string
defaultEtcDirectoryPath string
defaultBinDirectoryPath string
defaultConfigFilePath string
defaultKolideHosted bool
defaultAutoupdate bool
)

// Adapted from
// https://stackoverflow.com/questions/28322997/how-to-get-a-list-of-values-into-a-flag-in-golang/28323276#28323276
type arrayFlags []string
Expand All @@ -40,9 +51,17 @@ func (i *arrayFlags) Set(value string) error {
// parseOptions parses the options that may be configured via command-line flags
// and/or environment variables, determines order of precedence and returns a
// typed struct of options for further application use
func parseOptions(args []string) (*launcher.Options, error) {
flagset := flag.NewFlagSet("launcher", flag.ExitOnError)
flagset.Usage = func() { usage(flagset) }
func parseOptions(subcommandName string, args []string) (*launcher.Options, error) {
flagsetName := "launcher"
if subcommandName != "" {
flagsetName = fmt.Sprintf("launcher %s", subcommandName)
}
flagset := flag.NewFlagSet(flagsetName, flag.ExitOnError)
if subcommandName != "" {
flagset.Usage = func() { usage(flagset) }
} else {
flagset.Usage = commandUsage(flagset, flagsetName)
}

var (
// Primary options
Expand All @@ -57,13 +76,13 @@ func parseOptions(args []string) (*launcher.Options, error) {
flTransport = flagset.String("transport", "grpc", "The transport protocol that should be used to communicate with remote (default: grpc)")
flLoggingInterval = flagset.Duration("logging_interval", 60*time.Second, "The interval at which logs should be flushed to the server")
flOsquerydPath = flagset.String("osqueryd_path", "", "Path to the osqueryd binary to use (Default: find osqueryd in $PATH)")
flRootDirectory = flagset.String("root_directory", "", "The location of the local database, pidfiles, etc.")
flRootDirectory = flagset.String("root_directory", defaultRootDirectoryPath, "The location of the local database, pidfiles, etc.")
flRootPEM = flagset.String("root_pem", "", "Path to PEM file including root certificates to verify against")
flVersion = flagset.Bool("version", false, "Print Launcher version and exit")
flLogMaxBytesPerBatch = flagset.Int("log_max_bytes_per_batch", 0, "Maximum size of a batch of logs. Recommend leaving unset, and launcher will determine")
flOsqueryFlags arrayFlags // set below with flagset.Var
flCompactDbMaxTx = flagset.Int64("compactdb-max-tx", 65536, "Maximum transaction size used when compacting the internal DB")
_ = flagset.String("config", "", "config file to parse options from (optional)")
flConfigFilePath = flagset.String("config", defaultConfigFilePath, "config file to parse options from (optional)")

// osquery TLS endpoints
flOsqTlsConfig = flagset.String("config_tls_endpoint", "", "Config endpoint for the osquery tls transport")
Expand All @@ -73,7 +92,7 @@ func parseOptions(args []string) (*launcher.Options, error) {
flOsqTlsDistWrite = flagset.String("distributed_tls_write_endpoint", "", "Distributed write endpoint for the osquery tls transport")

// Autoupdate options
flAutoupdate = flagset.Bool("autoupdate", false, "Whether or not the osquery autoupdater is enabled (default: false)")
flAutoupdate = flagset.Bool("autoupdate", defaultAutoupdate, "Whether or not the osquery autoupdater is enabled (default: false)")
flNotaryServerURL = flagset.String("notary_url", autoupdate.DefaultNotary, "The Notary update server (default: https://notary.kolide.co)")
flTufServerURL = flagset.String("tuf_url", tuf.DefaultTufServer, "TUF update server (default: https://tuf.kolide.com)")
flMirrorURL = flagset.String("mirror_url", autoupdate.DefaultMirror, "The mirror server for autoupdates (default: https://dl.kolide.co)")
Expand Down Expand Up @@ -214,6 +233,7 @@ func parseOptions(args []string) (*launcher.Options, error) {
AutoupdateInitialDelay: *flAutoupdateInitialDelay,
CertPins: certPins,
CompactDbMaxTx: *flCompactDbMaxTx,
ConfigFilePath: *flConfigFilePath,
Control: false,
ControlServerURL: controlServerURL,
ControlRequestInterval: *flControlRequestInterval,
Expand Down
Loading

0 comments on commit 7a926d0

Please sign in to comment.