Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

launcher doctor subcommand #1197

Merged
merged 28 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment on lines +28 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, this is an interesting way to do this.

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