Skip to content

Commit

Permalink
support preprod dual install (#1834)
Browse files Browse the repository at this point in the history
  • Loading branch information
zackattack01 authored Aug 16, 2024
1 parent 1b40334 commit c48ff7c
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 57 deletions.
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) {
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

0 comments on commit c48ff7c

Please sign in to comment.