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

support preprod dual install #1834

Merged
merged 9 commits into from
Aug 16, 2024
94 changes: 37 additions & 57 deletions cmd/launcher/svc_config_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package main

import (
"context"
"fmt"
"log/slog"
"time"

Expand All @@ -15,8 +16,7 @@ import (
)

const (
launcherServiceName = `LauncherKolideK2Svc`
launcherServiceRegistryKeyName = `SYSTEM\CurrentControlSet\Services\LauncherKolideK2Svc`
launcherServiceRegistryKeyNameFmt = `SYSTEM\CurrentControlSet\Services\%s`

// DelayedAutostart is type REG_DWORD, i.e. uint32. We want to turn off delayed autostart.
delayedAutostartName = `DelayedAutostart`
Expand All @@ -35,6 +35,10 @@ func checkServiceConfiguration(logger *slog.Logger, opts *launcher.Options) {
return
}

// get the service name to generate the service key
launcherServiceName := launcher.ServiceName(opts.Identifier)
launcherServiceRegistryKeyName := fmt.Sprintf(launcherServiceRegistryKeyNameFmt, launcherServiceName)

// Get launcher service key
launcherServiceKey, err := registry.OpenKey(registry.LOCAL_MACHINE, launcherServiceRegistryKeyName, registry.ALL_ACCESS)
if err != nil {
Expand Down Expand Up @@ -64,9 +68,33 @@ func checkServiceConfiguration(logger *slog.Logger, opts *launcher.Options) {
// Check to see if we need to update the service to depend on Dnscache
checkDependOnService(launcherServiceKey, logger)

checkRestartActions(logger)
sman, err := mgr.Connect()
if err != nil {
logger.Log(context.TODO(), slog.LevelError,
"connecting to service control manager",
"err", err,
)

return
}

defer sman.Disconnect()

launcherService, err := sman.OpenService(launcherServiceName)
if err != nil {
logger.Log(context.TODO(), slog.LevelError,
"opening the launcher service from control manager",
"err", err,
)

setRecoveryActions(context.TODO(), logger)
return
}

defer launcherService.Close()

checkRestartActions(logger, launcherService)

setRecoveryActions(context.TODO(), logger, launcherService)
}

// checkDelayedAutostart checks the current value of `DelayedAutostart` (whether to wait ~2 minutes
Expand Down Expand Up @@ -137,32 +165,8 @@ func checkDependOnService(launcherServiceKey registry.Key, logger *slog.Logger)
// sets it to true if required. See https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_failure_actions_flag
// if we choose to implement restart backoff, that logic must be added here (it is not exposed via wix). See the "Windows Service Manager"
// doc in Notion for additional details on configurability
func checkRestartActions(logger *slog.Logger) {
sman, err := mgr.Connect()
if err != nil {
logger.Log(context.TODO(), slog.LevelError,
"connecting to service control manager",
"err", err,
)

return
}

defer sman.Disconnect()

launcherService, err := sman.OpenService(launcherServiceName)
if err != nil {
logger.Log(context.TODO(), slog.LevelError,
"opening the launcher service from control manager",
"err", err,
)

return
}

defer launcherService.Close()

curFlag, err := launcherService.RecoveryActionsOnNonCrashFailures()
func checkRestartActions(logger *slog.Logger, service *mgr.Service) {
curFlag, err := service.RecoveryActionsOnNonCrashFailures()
if err != nil {
logger.Log(context.TODO(), slog.LevelError,
"querying for current RecoveryActionsOnNonCrashFailures flag",
Expand All @@ -176,7 +180,7 @@ func checkRestartActions(logger *slog.Logger) {
return
}

if err = launcherService.SetRecoveryActionsOnNonCrashFailures(true); err != nil {
if err = service.SetRecoveryActionsOnNonCrashFailures(true); err != nil {
logger.Log(context.TODO(), slog.LevelError,
"setting RecoveryActionsOnNonCrashFailures flag",
"err", err,
Expand All @@ -190,31 +194,7 @@ func checkRestartActions(logger *slog.Logger) {

// setRecoveryActions sets the recovery actions for the launcher service.
// previously defined via wix ServicConfig Element (Util Extension) https://wixtoolset.org/docs/v3/xsd/util/serviceconfig/
func setRecoveryActions(ctx context.Context, logger *slog.Logger) {
sman, err := mgr.Connect()
if err != nil {
logger.Log(ctx, slog.LevelError,
"connecting to service control manager",
"err", err,
)

return
}

defer sman.Disconnect()

launcherService, err := sman.OpenService(launcherServiceName)
if err != nil {
logger.Log(ctx, slog.LevelError,
"opening the launcher service from control manager",
"err", err,
)

return
}

defer launcherService.Close()

func setRecoveryActions(ctx context.Context, logger *slog.Logger, service *mgr.Service) {
recoveryActions := []mgr.RecoveryAction{
{
// first failure
Expand All @@ -233,7 +213,7 @@ func setRecoveryActions(ctx context.Context, logger *slog.Logger) {
},
}

if err := launcherService.SetRecoveryActions(recoveryActions, 24*60*60); err != nil { // 24 hours
if err := service.SetRecoveryActions(recoveryActions, 24*60*60); err != nil { // 24 hours
logger.Log(ctx, slog.LevelError,
"setting RecoveryActions",
"err", err,
Expand Down
55 changes: 55 additions & 0 deletions ee/debug/checkups/checkups.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ import (
"fmt"
"io"
"log/slog"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"time"

"github.com/kolide/kit/version"
"github.com/kolide/launcher/ee/agent/types"
"github.com/kolide/launcher/pkg/launcher"
)

type Status string
Expand Down Expand Up @@ -302,10 +305,62 @@ func RunFlare(ctx context.Context, k types.Knapsack, flareStream io.WriteCloser,
}
}

// note we do not check errors or do anything to complicate the normal flare process
// from the multiple installation check. this is not (at this time) an expected production complication
noteMultipleInstallations(flare)

// we could defer this close, but we want to return any errors
return close()
}

// noteMultipleInstallations checks for whether the results of running flare for this installation may be complicated
// by multiple installations. This is less of an issue when the flare is run in situ, but should be noted because
// we may need to pay closer attention to the results. When run standalone without a config argument passed, it would be
// possible for flare to default to reading the directories for the wrong installation
func noteMultipleInstallations(z *zip.Writer) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Feels weird not to push this through the normal checkup interface. But okay...

defaultPath := strings.TrimSuffix(launcher.DefaultPath(launcher.BinDirectory), string(filepath.Separator))
pathParts := strings.Split(defaultPath, string(filepath.Separator))

if len(pathParts) < 3 {
return
}

if runtime.GOOS == "windows" { // strip the bin for windows
pathParts = pathParts[:len(pathParts)-1]
}

// now strip off the directory which should note the identifier. we are
// going to list everything in the parent directory and count kolide references
// to get an idea of the total potential installations
pathParts = pathParts[:len(pathParts)-1]

// now put the path back together
baseDir := strings.Join(pathParts, string(filepath.Separator))

entries, err := os.ReadDir(baseDir)
if err != nil {
return
}

matchingDirs := make([]string, 0)
for _, e := range entries {
if strings.Contains(strings.ToLower(e.Name()), "kolide") {
matchingDirs = append(matchingDirs, e.Name())
}
}

if len(matchingDirs) <= 1 {
return // nothing to note, standard single installation
}

w, err := z.Create("MULTIPLE_LAUNCHER_INSTALLS_DETECTED")
if err != nil {
return
}

json.NewEncoder(w).Encode(matchingDirs)
}

func writeFlareEnv(z *zip.Writer, runtimeEnvironment runtimeEnvironmentType) error {
if _, err := z.Create(fmt.Sprintf("FLARE_RUNNING_%s", strings.ReplaceAll(strings.ToUpper(string(runtimeEnvironment)), " ", "_"))); err != nil {
return fmt.Errorf("making env note file: %s", err)
Expand Down
11 changes: 11 additions & 0 deletions ee/debug/shipper/shipper.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,17 @@ func enrollSecret(k types.Knapsack) string {
return k.EnrollSecret()
}

if k != nil && k.EnrollSecretPath() != "" {
secret, err := os.ReadFile(k.EnrollSecretPath())
if err != nil {
return ""
}

return string(secret)
}

// TODO this will need to respect the identifier when determining the secret file location for dual-launcher installations
// this will specifically be an issue when flare is triggered standalone (without config path specified)
b, err := os.ReadFile(launcher.DefaultPath(launcher.SecretFile))
if err != nil {
return ""
Expand Down
2 changes: 2 additions & 0 deletions ee/debug/shipper/shipper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func TestShip(t *testing.T) { //nolint:paralleltest
mockKnapsack: func(t *testing.T) *typesMocks.Knapsack {
k := typesMocks.NewKnapsack(t)
k.On("EnrollSecret").Return("")
k.On("EnrollSecretPath").Return("")
return k
},
assertion: assert.NoError,
Expand All @@ -45,6 +46,7 @@ func TestShip(t *testing.T) { //nolint:paralleltest
mockKnapsack: func(t *testing.T) *typesMocks.Knapsack {
k := typesMocks.NewKnapsack(t)
k.On("EnrollSecret").Return("")
k.On("EnrollSecretPath").Return("")
return k
},
assertion: assert.NoError,
Expand Down
7 changes: 7 additions & 0 deletions pkg/launcher/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"github.com/peterbourgon/ff/v3"
)

const DefaultLauncherIdentifier string = "kolide-k2"

// Options is the set of options that may be configured for Launcher.
type Options struct {
// KolideServerURL is the URL of the management server to connect to.
Expand Down Expand Up @@ -134,6 +136,9 @@ type Options struct {

// LocalDevelopmentPath is the path to a local build of launcher to test against, rather than finding the latest version in the library
LocalDevelopmentPath string

// Identifier is the key used to identify/namespace a single launcher installation (e.g. kolide-k2)
Identifier string
}

// ConfigFilePath returns the path to launcher's launcher.flags file. If the path
Expand Down Expand Up @@ -254,6 +259,7 @@ func ParseOptions(subcommandName string, args []string) (*Options, error) {
flIAmBreakingEELicense = flagset.Bool("i-am-breaking-ee-license", false, "Skip license check before running localserver (default: false)")
flDelayStart = flagset.Duration("delay_start", 0*time.Second, "How much time to wait before starting launcher")
flLocalDevelopmentPath = flagset.String("localdev_path", "", "Path to local launcher build")
flPackageIdentifier = flagset.String("identifier", DefaultLauncherIdentifier, "packaging identifier used to determine service names, paths, etc. (default: kolide-k2)")

// deprecated options, kept for any kind of config file compatibility
_ = flagset.String("debug_log_file", "", "DEPRECATED")
Expand Down Expand Up @@ -388,6 +394,7 @@ func ParseOptions(subcommandName string, args []string) (*Options, error) {
Debug: *flDebug,
DelayStart: *flDelayStart,
DisableControlTLS: disableControlTLS,
Identifier: *flPackageIdentifier,
InsecureControlTLS: insecureControlTLS,
EnableInitialRunner: *flInitialRunner,
WatchdogEnabled: *flWatchdogEnabled,
Expand Down
1 change: 1 addition & 0 deletions pkg/launcher/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ func getArgsAndResponse() (map[string]string, *Options) {
WatchdogDelaySec: 120,
WatchdogMemoryLimitMB: 600,
WatchdogUtilizationLimitPercent: 50,
Identifier: DefaultLauncherIdentifier,
}

return args, opts
Expand Down
27 changes: 27 additions & 0 deletions pkg/launcher/pkg_utils_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//go:build windows
// +build windows

package launcher

import (
"fmt"
"regexp"
"strings"

"github.com/serenize/snaker"
)

var nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9]+`)

// ServiceName embeds the given identifier into our service name template after sanitization,
// and returns the camelCased service name generated to match our packaging logic
func ServiceName(identifier string) string {
// this check might be overkill but is intended to prevent any backwards compatibility/misconfiguration issues
if strings.TrimSpace(identifier) == "" {
identifier = DefaultLauncherIdentifier
}

sanitizedServiceName := nonAlphanumericRegex.ReplaceAllString(identifier, "_") // e.g. identifier=kolide-k2 becomes kolide_k2
sanitizedServiceName = fmt.Sprintf("launcher_%s_svc", sanitizedServiceName) // wrapped as launcher_kolide_k2_svc
return snaker.SnakeToCamel(sanitizedServiceName) // will produce LauncherKolideK2Svc
}
49 changes: 49 additions & 0 deletions pkg/launcher/pkg_utils_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//go:build windows
// +build windows

package launcher

import (
"testing"

"github.com/stretchr/testify/require"
)

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

for _, tt := range []struct {
testCaseName string
identifier string
expectedServiceName string
}{
{
testCaseName: "empty identifier expecting default service name",
identifier: " ",
expectedServiceName: "LauncherKolideK2Svc",
},
{
testCaseName: "default identifier expecting default service name",
identifier: "kolide-k2",
expectedServiceName: "LauncherKolideK2Svc",
},
{
testCaseName: "preprod identifier expecting preprod service name",
identifier: "kolide-preprod-k2",
expectedServiceName: "LauncherKolidePreprodK2Svc",
},
{
testCaseName: "mangled identifier expecting default service name",
identifier: "kolide-!@_k2",
expectedServiceName: "LauncherKolideK2Svc",
},
} {
tt := tt
t.Run(tt.testCaseName, func(t *testing.T) {
t.Parallel()

serviceName := ServiceName(tt.identifier)
require.Equal(t, tt.expectedServiceName, serviceName, "expected sanitized service name value to match")
})
}
}
Loading
Loading