Merge pull request #26440 from hashicorp/f-winsvc-service

Add Windows service commands and Event Log support
This commit is contained in:
Chris Roberts
2025-09-02 17:10:19 -07:00
committed by GitHub
44 changed files with 3835 additions and 27 deletions

3
.changelog/26441.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
agent: Allow agent logging to the Windows Event Log
```

3
.changelog/26442.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
cli: Add commands for installing and uninstalling Windows system service
```

View File

@@ -13,6 +13,7 @@ project {
"ui/node_modules",
"pnpm-workspace.yaml",
"pnpm-lock.yaml",
"helper/winsvc/strings_*.go",
// Enterprise files do not fall under the open source licensing. CE-ENT
// merge conflicts might happen here, please be sure to put new CE

View File

@@ -75,6 +75,7 @@ func (c *Command) readConfig() *Config {
ACL: &ACLConfig{},
Audit: &config.AuditConfig{},
Reporting: &config.ReportingConfig{},
Eventlog: &Eventlog{},
}
flags := flag.NewFlagSet("agent", flag.ContinueOnError)
@@ -132,6 +133,10 @@ func (c *Command) readConfig() *Config {
flags.BoolVar(&cmdConfig.LogIncludeLocation, "log-include-location", false, "")
flags.StringVar(&cmdConfig.NodeName, "node", "", "")
// Eventlog options
flags.BoolVar(&cmdConfig.Eventlog.Enabled, "eventlog", false, "")
flags.StringVar(&cmdConfig.Eventlog.Level, "eventlog-level", "", "")
// Consul options
defaultConsul := cmdConfig.defaultConsul()
flags.StringVar(&defaultConsul.Auth, "consul-auth", "", "")
@@ -489,7 +494,12 @@ func (c *Command) IsValidConfig(config, cmdConfig *Config) bool {
return false
}
if err := config.RPC.Validate(); err != nil {
c.Ui.Error(fmt.Sprintf("rpc block invalid: %v)", err))
c.Ui.Error(fmt.Sprintf("rpc block invalid: %v", err))
return false
}
if err := config.Eventlog.Validate(); err != nil {
c.Ui.Error(fmt.Sprintf("eventlog block invalid: %v", err))
return false
}
@@ -587,6 +597,7 @@ func SetupLoggers(ui cli.Ui, config *Config) (*gatedwriter.Writer, io.Writer) {
if logLevel == "OFF" {
config.EnableSyslog = false
}
// Check if syslog is enabled
if config.EnableSyslog {
ui.Output(fmt.Sprintf("Config enable_syslog is `true` with log_level=%v", config.LogLevel))
@@ -598,6 +609,17 @@ func SetupLoggers(ui cli.Ui, config *Config) (*gatedwriter.Writer, io.Writer) {
writers = append(writers, newSyslogWriter(l, config.LogJson))
}
// Check if eventlog is enabled
if config.Eventlog != nil && config.Eventlog.Enabled {
l, err := winsvc.NewEventLogger(config.Eventlog.Level)
if err != nil {
ui.Error(fmt.Sprintf("Windows event logger setup failed: %s", err))
return nil, nil
}
writers = append(writers, l)
}
// Check if file logging is enabled
if config.LogFile != "" {
dir, fileName := filepath.Split(config.LogFile)
@@ -773,6 +795,8 @@ func (c *Command) AutocompleteFlags() complete.Flags {
"-vault-tls-server-name": complete.PredictAnything,
"-acl-enabled": complete.PredictNothing,
"-acl-replication-token": complete.PredictAnything,
"-eventlog": complete.PredictNothing,
"-eventlog-level": complete.PredictSet("INFO", "WARN", "ERROR"),
}
}
@@ -920,6 +944,10 @@ func (c *Command) Run(args []string) int {
return 1
}
// Add events for the eventlog
winsvc.SendEvent(winsvc.NewEvent(winsvc.EventServiceReady))
defer func() { winsvc.SendEvent(winsvc.NewEvent(winsvc.EventServiceStopped)) }()
// Wait for exit
return c.handleSignals()
}
@@ -1481,6 +1509,14 @@ General Options (clients and servers):
-log-include-location
Include file and line information in each log line. The default is false.
-eventlog
Enable sending Nomad agent logs to the Windows Event Log.
-eventlog-level
Specifies the verbosity of logs the Nomad agent outputs. Valid log levels
include ERROR, WARN, or INFO in order of verbosity. Level must be
of equal or less verbosity as defined for the -log-level parameter.
-node=<name>
The name of the local agent. This name is used to identify the node
in the cluster. The name must be unique per region. The default is

View File

@@ -77,6 +77,10 @@ func TestCommand_Args(t *testing.T) {
[]string{"-client", "-node-pool=not@valid"},
"Invalid node pool",
},
{
[]string{"-client", "-eventlog-level", "DEBUG"},
"eventlog.level must be one of INFO, WARN, or ERROR",
},
}
for _, tc := range tcases {
// Make a new command. We preemptively close the shutdownCh
@@ -502,6 +506,33 @@ func TestIsValidConfig(t *testing.T) {
},
err: "unknown keyring provider",
},
{
name: "ValidEventlog",
conf: Config{
DataDir: "/tmp",
Client: &ClientConfig{
Enabled: true,
},
Eventlog: &Eventlog{
Enabled: true,
Level: "INFO",
},
},
},
{
name: "InvalidEventlog",
conf: Config{
DataDir: "/tmp",
Client: &ClientConfig{
Enabled: true,
},
Eventlog: &Eventlog{
Enabled: true,
Level: "DEBUG",
},
},
err: "eventlog.level must be one of INFO, WARN, or ERROR",
},
}
for _, tc := range cases {

View File

@@ -29,6 +29,7 @@ import (
"github.com/hashicorp/nomad/helper/ipaddr"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/hashicorp/nomad/helper/users"
"github.com/hashicorp/nomad/helper/winsvc"
"github.com/hashicorp/nomad/nomad"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
@@ -199,6 +200,9 @@ type Config struct {
// ExtraKeysHCL is used by hcl to surface unexpected keys
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
// Configure logging to Windows eventlog
Eventlog *Eventlog `hcl:"eventlog"`
}
func (c *Config) defaultConsul() *config.ConsulConfig {
@@ -1399,6 +1403,60 @@ func (t *Telemetry) Validate() error {
return nil
}
// Eventlog is the configuration for the Windows Eventlog
type Eventlog struct {
// Enabled controls if Nomad agent logs are sent to the
// Windows eventlog.
Enabled bool `hcl:"enabled"`
// Level of logs to send to eventlog. May be set to higher
// severity than LogLevel but lower level will be ignored.
Level string `hcl:"level"`
}
// Copy is used to copy the Eventlog configuration
func (e *Eventlog) Copy() *Eventlog {
return &Eventlog{
Enabled: e.Enabled,
Level: e.Level,
}
}
// Merge is used to merge Eventlog configurations
func (e *Eventlog) Merge(b *Eventlog) *Eventlog {
if e == nil {
return b
}
result := *e
if b == nil {
return &result
}
if b.Enabled {
result.Enabled = b.Enabled
}
if b.Level != "" {
result.Level = b.Level
}
return &result
}
// Validate validates the eventlog configuration
func (e *Eventlog) Validate() error {
if e == nil {
return nil
}
if winsvc.EventlogLevelFromString(e.Level) == winsvc.EVENTLOG_LEVEL_UNKNOWN {
return errors.New("eventlog.level must be one of INFO, WARN, or ERROR")
}
return nil
}
// Ports encapsulates the various ports we bind to for network services. If any
// are not specified then the defaults are used instead.
type Ports struct {
@@ -1732,6 +1790,10 @@ func DefaultConfig() *Config {
collectionInterval: 1 * time.Second,
DisableAllocationHookMetrics: pointer.Of(false),
},
Eventlog: &Eventlog{
Enabled: false,
Level: "error",
},
TLSConfig: &config.TLSConfig{},
Sentinel: &config.SentinelConfig{},
Version: version.GetVersion(),
@@ -1844,6 +1906,13 @@ func (c *Config) Merge(b *Config) *Config {
result.Telemetry = result.Telemetry.Merge(b.Telemetry)
}
// Apply the eventlog config
if result.Eventlog == nil && b.Eventlog != nil {
result.Eventlog = b.Eventlog.Copy()
} else if b.Eventlog != nil {
result.Eventlog = result.Eventlog.Merge(b.Eventlog)
}
// Apply the Reporting Config
if result.Reporting == nil && b.Reporting != nil {
result.Reporting = b.Reporting.Copy()

View File

@@ -48,6 +48,7 @@ func TestConfig_Merge(t *testing.T) {
AdvertiseAddrs: &AdvertiseAddrs{},
Sentinel: &config.SentinelConfig{},
Autopilot: &config.AutopilotConfig{},
Eventlog: &Eventlog{},
}
c2 := &Config{
@@ -236,6 +237,10 @@ func TestConfig_Merge(t *testing.T) {
},
},
},
Eventlog: &Eventlog{
Enabled: true,
Level: "INFO",
},
}
c3 := &Config{
@@ -489,6 +494,10 @@ func TestConfig_Merge(t *testing.T) {
Enabled: pointer.Of(true),
},
},
Eventlog: &Eventlog{
Enabled: true,
Level: "ERROR",
},
}
result := c0.Merge(c1)
@@ -2013,3 +2022,86 @@ func TestConfig_LoadClientNodeMaxAllocs(t *testing.T) {
}
}
func TestEventlog_Merge(t *testing.T) {
t.Run("nil rhs merge", func(t *testing.T) {
var c1, c2 *Eventlog
c1 = &Eventlog{
Enabled: true,
Level: "info",
}
result := c1.Merge(c2)
must.Eq(t, result, c1)
})
t.Run("nil lhs merge", func(t *testing.T) {
var c1, c2 *Eventlog
c2 = &Eventlog{
Enabled: true,
Level: "info",
}
result := c1.Merge(c2)
must.Eq(t, result, c2)
})
t.Run("full merge", func(t *testing.T) {
c1 := &Eventlog{
Enabled: false,
Level: "info",
}
c2 := &Eventlog{
Enabled: true,
Level: "error",
}
result := c1.Merge(c2)
must.True(t, result.Enabled)
must.Eq(t, result.Level, "error")
})
t.Run("enabled merge", func(t *testing.T) {
// NOTE: Can only be enabled, not disabled
c1 := &Eventlog{
Enabled: true,
}
c2 := &Eventlog{
Enabled: false,
}
result := c1.Merge(c2)
must.True(t, result.Enabled)
})
}
func TestEventlog_Validate(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
desc string
eventlog *Eventlog
shouldErr bool
}{
{
desc: "valid level",
eventlog: &Eventlog{Level: "info"},
},
{
desc: "invalid level",
eventlog: &Eventlog{Level: "debug"},
shouldErr: true,
},
{
desc: "nil eventlog",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
ci.Parallel(t)
if tc.shouldErr {
must.Error(t, tc.eventlog.Validate())
} else {
must.NoError(t, tc.eventlog.Validate())
}
})
}
}

View File

@@ -5,7 +5,9 @@ package command
import (
"fmt"
"maps"
"os"
"runtime"
"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/command/agent"
@@ -1339,6 +1341,31 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
},
}
if runtime.GOOS == "windows" {
maps.Copy(all, map[string]cli.CommandFactory{
"windows": func() (cli.Command, error) {
return &WindowsCommand{
Meta: meta,
}, nil
},
"windows service": func() (cli.Command, error) {
return &WindowsServiceCommand{
Meta: meta,
}, nil
},
"windows service install": func() (cli.Command, error) {
return &WindowsServiceInstallCommand{
Meta: meta,
}, nil
},
"windows service uninstall": func() (cli.Command, error) {
return &WindowsServiceUninstallCommand{
Meta: meta,
}, nil
},
})
}
deprecated := map[string]cli.CommandFactory{
"client-config": func() (cli.Command, error) {
return &DeprecatedCommand{

35
command/windows.go Normal file
View File

@@ -0,0 +1,35 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"strings"
"github.com/hashicorp/cli"
)
type WindowsCommand struct {
Meta
}
func (c *WindowsCommand) Help() string {
helpText := `
Usage: nomad windows <subcommand> [options]
This command groups subcommands for managing Nomad as a system service on Windows.
Service::
$ nomad windows service
Refer to the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *WindowsCommand) Name() string { return "windows" }
func (c *WindowsCommand) Synopsis() string { return "Manage Nomad as a system service on Windows" }
func (c *WindowsCommand) Run(_ []string) int { return cli.RunResultHelp }

View File

@@ -0,0 +1,41 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"strings"
"github.com/hashicorp/cli"
)
type WindowsServiceCommand struct {
Meta
}
func (c *WindowsServiceCommand) Help() string {
helpText := `
Usage: nomad windows service <subcommand> [options]
This command groups subcommands for managing Nomad as a system service on Windows.
Install:
$ nomad windows service install
Uninstall:
$ nomad windows service uninstall
Refer to the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *WindowsServiceCommand) Name() string { return "windows service" }
func (c *WindowsServiceCommand) Synopsis() string {
return "Manage nomad as a system service on Windows"
}
func (c *WindowsServiceCommand) Run(_ []string) int { return cli.RunResultHelp }

View File

@@ -0,0 +1,365 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/nomad/helper/winsvc"
"github.com/posener/complete"
)
type windowsInstallOpts struct {
configDir, dataDir, installDir, binaryPath string
reinstall bool
}
type WindowsServiceInstallCommand struct {
Meta
serviceManagerFn func() (winsvc.WindowsServiceManager, error)
privilegedCheckFn func() bool
winPaths winsvc.WindowsPaths
}
func (c *WindowsServiceInstallCommand) Synopsis() string {
return "Install the Nomad Windows system service"
}
func (c *WindowsServiceInstallCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetDefault),
complete.Flags{
"-config-dir": complete.PredictDirs("*"),
"-data-dir": complete.PredictDirs("*"),
"-install-dir": complete.PredictDirs("*"),
"-reinstall": complete.PredictNothing,
})
}
func (c *WindowsServiceInstallCommand) Name() string { return "windows service install" }
func (c *WindowsServiceInstallCommand) Help() string {
helpText := `
Usage: nomad windows service install [options]
This command installs Nomad as a Windows system service.
General Options:
` + generalOptionsUsage(usageOptsDefault) + `
Service Install Options:
-config-dir <dir>
Directory to hold the Nomad agent configuration. Defaults
to "{{.ProgramFiles}}\HashiCorp\nomad\bin"
-data-dir <dir>
Directory to hold the Nomad agent state. Defaults
to "{{.ProgramData}}\HashiCorp\nomad\data"
-install-dir <dir>
Directory to install the Nomad binary. Defaults
to "{{.ProgramData}}\HashiCorp\nomad\config"
-reinstall
Allow the nomad Windows service to be stopped during install.
`
return strings.TrimSpace(helpText)
}
func (c *WindowsServiceInstallCommand) Run(args []string) int {
opts := &windowsInstallOpts{}
flags := c.Meta.FlagSet(c.Name(), FlagSetDefault)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.StringVar(&opts.configDir, "config-dir", "", "")
flags.StringVar(&opts.dataDir, "data-dir", "", "")
flags.StringVar(&opts.installDir, "install-dir", "", "")
flags.BoolVar(&opts.reinstall, "reinstall", false, "")
if err := flags.Parse(args); err != nil {
return 1
}
if args = flags.Args(); len(args) > 0 {
c.Ui.Error("This command takes no arguments")
c.Ui.Error(commandErrorText(c))
return 1
}
// Set helper functions to defaults if unset
if c.winPaths == nil {
c.winPaths = winsvc.NewWindowsPaths()
}
if c.serviceManagerFn == nil {
c.serviceManagerFn = winsvc.NewWindowsServiceManager
}
if c.privilegedCheckFn == nil {
c.privilegedCheckFn = winsvc.IsPrivilegedProcess
}
// Check that command is being run with elevated permissions
if !c.privilegedCheckFn() {
c.Ui.Error("Service install must be run with Administator privileges")
return 1
}
c.Ui.Output("Installing nomad as a Windows service...")
m, err := c.serviceManagerFn()
if err != nil {
c.Ui.Error(fmt.Sprintf("Could not connect to Windows service manager - %s", err))
return 1
}
defer m.Close()
if err := c.performInstall(m, opts); err != nil {
c.Ui.Error(fmt.Sprintf("Service install failed: %s", err))
return 1
}
c.Ui.Info("Successfully installed nomad Windows service")
return 0
}
func (c *WindowsServiceInstallCommand) performInstall(m winsvc.WindowsServiceManager, opts *windowsInstallOpts) error {
// Check if the nomad service has already been
// registered. If so the service needs to be
// stopped before proceeding with the install.
exists, err := m.IsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("unable to check for existing service - %w", err)
}
if exists {
nmdSvc, err := m.GetService(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("could not get existing service to stop - %w", err)
}
// If the service is running and the reinstall
// option was not set, return an error
running, err := nmdSvc.IsRunning()
if err != nil {
return fmt.Errorf("unable to determine service state - %w", err)
}
if running && !opts.reinstall {
return fmt.Errorf("service is already running. Please run the\ncommand again with -reinstall to stop the service and install.")
}
if running {
c.Ui.Output(" Stopping existing nomad service")
if err := nmdSvc.Stop(); err != nil {
return fmt.Errorf("unable to stop existing service - %w", err)
}
}
}
// Install the nomad binary into the system
if err = c.binaryInstall(opts); err != nil {
return fmt.Errorf("binary install failed - %w", err)
}
c.Ui.Output(fmt.Sprintf(" Nomad binary installed to: %s", opts.binaryPath))
// Create a configuration directory and add
// a basic configuration file if no configuration
// currently exists
if err = c.configInstall(opts); err != nil {
return fmt.Errorf("configuration install failed - %w", err)
}
c.Ui.Output(fmt.Sprintf(" Nomad configuration directory: %s", opts.configDir))
c.Ui.Output(fmt.Sprintf(" Nomad agent data directory: %s", opts.configDir))
// Now let's install that service
if err := c.serviceInstall(m, opts); err != nil {
return fmt.Errorf("service install failed - %w", err)
}
return nil
}
func (c *WindowsServiceInstallCommand) serviceInstall(m winsvc.WindowsServiceManager, opts *windowsInstallOpts) error {
var err error
var srvc winsvc.WindowsService
cmd := fmt.Sprintf("%s agent -config %s", opts.binaryPath, opts.configDir)
exists, err := m.IsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("service registration check failed - %w", err)
}
// If the service already exists, open it and update. Otherwise
// create a new service.
if exists {
srvc, err = m.GetService(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("unable to get existing service - %w", err)
}
defer srvc.Close()
if err := srvc.Configure(winsvc.WindowsServiceConfiguration{
StartType: winsvc.StartAutomatic,
DisplayName: winsvc.WINDOWS_SERVICE_DISPLAY_NAME,
Description: winsvc.WINDOWS_SERVICE_DESCRIPTION,
BinaryPathName: cmd,
}); err != nil {
return fmt.Errorf("unable to configure service - %w", err)
}
} else {
srvc, err = m.CreateService(winsvc.WINDOWS_SERVICE_NAME, opts.binaryPath,
winsvc.WindowsServiceConfiguration{
StartType: winsvc.StartAutomatic,
DisplayName: winsvc.WINDOWS_SERVICE_DISPLAY_NAME,
Description: winsvc.WINDOWS_SERVICE_DESCRIPTION,
BinaryPathName: cmd,
},
)
if err != nil {
return fmt.Errorf("unable to create service - %w", err)
}
defer srvc.Close()
}
// Enable the service in the Windows eventlog
if err := srvc.EnableEventlog(); err != nil {
return fmt.Errorf("could not configure eventlog - %w", err)
}
// Ensure the service is stopped
if err := srvc.Stop(); err != nil {
return fmt.Errorf("could not stop service - %w", err)
}
// Start the service so the new binary is in use
if err := srvc.Start(); err != nil {
return fmt.Errorf("could not start service - %w", err)
}
return nil
}
func (c *WindowsServiceInstallCommand) configInstall(opts *windowsInstallOpts) error {
// If the config or data directory are unset, default them
if opts.configDir == "" {
opts.configDir = filepath.Join(winsvc.WINDOWS_INSTALL_APPDATA_DIRECTORY, "config")
}
if opts.dataDir == "" {
opts.dataDir = filepath.Join(winsvc.WINDOWS_INSTALL_APPDATA_DIRECTORY, "data")
}
var err error
if opts.configDir, err = c.winPaths.Expand(opts.configDir); err != nil {
return fmt.Errorf("cannot generate configuration path - %s", err)
}
if opts.dataDir, err = c.winPaths.Expand(opts.dataDir); err != nil {
return fmt.Errorf("cannot generate data path - %s", err)
}
// Ensure directories exist
if err = c.winPaths.CreateDirectory(opts.configDir, true); err != nil {
return fmt.Errorf("cannot create configuration directory - %s", err)
}
if err = c.winPaths.CreateDirectory(opts.dataDir, true); err != nil {
return fmt.Errorf("cannot create data directory - %s", err)
}
// Check if any configuration files exist
matches, _ := filepath.Glob(filepath.Join(opts.configDir, "*"))
if len(matches) < 1 {
f, err := os.Create(filepath.Join(opts.configDir, "config.hcl"))
if err != nil {
return fmt.Errorf("could not create default configuration file - %s", err)
}
fmt.Fprintf(f, strings.TrimSpace(`
# Full configuration options can be found at https://developer.hashicorp.com/nomad/docs/configuration
data_dir = "%s"
bind_addr = "0.0.0.0"
server {
# license_path is required for Nomad Enterprise as of Nomad v1.1.1+
#license_path = "%s\license.hclic"
enabled = true
bootstrap_expect = 1
}
client {
enabled = true
servers = ["127.0.0.1"]
}
log_level = "WARN"
eventlog {
enabled = true
level = "ERROR"
}
`), strings.ReplaceAll(opts.dataDir, `\`, `\\`), strings.ReplaceAll(opts.configDir, `\`, `\\`))
f.Close()
c.Ui.Output(fmt.Sprintf(" Added initial configuration file: %s", f.Name()))
}
return nil
}
func (c *WindowsServiceInstallCommand) binaryInstall(opts *windowsInstallOpts) error {
// Get the path to the currently executing nomad. This
// will be installed for the service to call.
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("cannot detect current nomad path - %s", err)
}
// Build the needed paths
if opts.installDir == "" {
opts.installDir = winsvc.WINDOWS_INSTALL_BIN_DIRECTORY
}
opts.installDir, err = c.winPaths.Expand(opts.installDir)
if err != nil {
return fmt.Errorf("cannot generate binary install path - %s", err)
}
opts.binaryPath = filepath.Join(opts.installDir, "nomad.exe")
// Ensure the install directory exists
if err = c.winPaths.CreateDirectory(opts.installDir, false); err != nil {
return fmt.Errorf("could not create binary install directory - %s", err)
}
// Create a new copy of the current binary to install
exeFile, err := os.Open(exePath)
if err != nil {
return fmt.Errorf("cannot open current nomad path for install - %s", err)
}
defer exeFile.Close()
// Copy into a temporary file which can then be moved
// into the correct location.
dstFile, err := os.CreateTemp(os.TempDir(), "nomad*")
if err != nil {
return fmt.Errorf("cannot create copy - %s", err)
}
defer dstFile.Close()
if _, err = io.Copy(dstFile, exeFile); err != nil {
return fmt.Errorf("cannot write nomad binary for install - %s", err)
}
dstFile.Close()
// With a copy ready to be moved into place, ensure that
// the path is clear then move the file.
if err = os.RemoveAll(opts.binaryPath); err != nil {
return fmt.Errorf("cannot remove existing nomad binary install - %s", err)
}
if err = os.Rename(dstFile.Name(), opts.binaryPath); err != nil {
return fmt.Errorf("cannot install new nomad binary - %s", err)
}
return nil
}

View File

@@ -0,0 +1,391 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"bytes"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"text/template"
"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/helper/winsvc"
"github.com/shoenig/test/must"
)
func TestWindowsServiceInstallCommand_Run(t *testing.T) {
t.Parallel()
freshInstallFn := func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectCreateService(winsvc.WINDOWS_SERVICE_NAME, "nomad.exe",
winsvc.WindowsServiceConfiguration{}, srv, nil)
srv.ExpectEnableEventlog(nil)
srv.ExpectStop(nil)
srv.ExpectStart(nil)
}
upgradeInstallFn := func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectIsRunning(false, nil)
srv.ExpectConfigure(winsvc.WindowsServiceConfiguration{}, nil)
srv.ExpectEnableEventlog(nil)
srv.ExpectStop(nil)
srv.ExpectStart(nil)
}
testCases := []struct {
desc string
args []string
privilegeFn func() bool
setup func(string, *winsvc.MockWindowsServiceManager)
after func(string)
output string
errOutput string
status int
}{
{
desc: "fresh install success",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
output: "Success",
status: 0,
},
{
desc: "fresh install writes config",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
after: func(dir string) {
must.FileExists(t, filepath.Join(dir, "programdata/HashiCorp/nomad/config/config.hcl"))
},
output: "initial configuration file",
},
{
desc: "fresh install binary file",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
after: func(dir string) {
must.FileExists(t, filepath.Join(dir, "programfiles/HashiCorp/nomad/bin/nomad.exe"))
},
output: "binary installed",
},
{
desc: "fresh install configuration already exists",
setup: func(dir string, m *winsvc.MockWindowsServiceManager) {
cdir := filepath.Join(dir, "programdata/HashiCorp/nomad/config")
err := os.MkdirAll(cdir, 0o755)
must.NoError(t, err)
f, err := os.Create(filepath.Join(cdir, "custom.hcl"))
must.NoError(t, err)
f.Close()
freshInstallFn(m)
},
after: func(dir string) {
must.FileNotExists(t, filepath.Join(dir, "programdata/HashiCorp/nomad/config/config.hcl"))
},
},
{
desc: "fresh install binary already exists",
setup: func(dir string, m *winsvc.MockWindowsServiceManager) {
cdir := filepath.Join(dir, "programfiles/HashiCorp/nomad/bin")
err := os.MkdirAll(cdir, 0o755)
must.NoError(t, err)
// create empty binary file
f, err := os.Create(filepath.Join(cdir, "nomad.exe"))
must.NoError(t, err)
f.Close()
freshInstallFn(m)
},
after: func(dir string) {
s, err := os.Stat(filepath.Join(dir, "programfiles/HashiCorp/nomad/bin/nomad.exe"))
must.NoError(t, err)
// ensure binary file is not empty
must.NonZero(t, s.Size())
},
},
{
desc: "upgrade install success",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
upgradeInstallFn(m)
},
output: "Success",
status: 0,
},
{
desc: "with arguments",
args: []string{"any", "value"},
errOutput: "takes no arguments",
status: 1,
},
{
desc: "with -install-dir",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
args: []string{"-install-dir", "{{.ProgramFiles}}/custom/bin"},
after: func(dir string) {
_, err := os.Stat(filepath.Join(dir, "programfiles/custom/bin/nomad.exe"))
must.NoError(t, err)
},
},
{
desc: "with -config-dir",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
args: []string{"-config-dir", "{{.ProgramData}}/custom/nomad-configuration"},
after: func(dir string) {
_, err := os.Stat(filepath.Join(dir, "programdata/custom/nomad-configuration"))
must.NoError(t, err)
},
},
{
desc: "with -data-dir",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
freshInstallFn(m)
},
args: []string{"-data-dir", "{{.ProgramData}}/custom/nomad-data"},
after: func(dir string) {
_, err := os.Stat(filepath.Join(dir, "programdata/custom/nomad-data"))
must.NoError(t, err)
},
},
{
desc: "service registered check failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, errors.New("lookup failure"))
},
errOutput: "unable to check for existing service",
status: 1,
},
{
desc: "service registered check failure during service install",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, errors.New("lookup failure"))
},
errOutput: "registration check failed",
status: 1,
},
{
desc: "get existing service to stop failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, nil, errors.New("service get failure"))
},
errOutput: "could not get existing service",
status: 1,
},
{
desc: "stop existing service failure",
args: []string{"-reinstall"},
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectIsRunning(true, nil)
srv.ExpectStop(errors.New("cannot stop"))
},
errOutput: "unable to stop existing service",
status: 1,
},
{
desc: "get existing service to configure failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectIsRunning(false, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, nil, errors.New("service get failure"))
},
errOutput: "unable to get existing service",
status: 1,
},
{
desc: "configure service failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
srv.ExpectIsRunning(false, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectConfigure(winsvc.WindowsServiceConfiguration{}, errors.New("configure failure"))
},
errOutput: "unable to configure service",
status: 1,
},
{
desc: "create service failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectCreateService(winsvc.WINDOWS_SERVICE_NAME, "nomad.exe", winsvc.WindowsServiceConfiguration{}, nil, errors.New("create service failure"))
},
errOutput: "unable to create service",
status: 1,
},
{
desc: "eventlog setup failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectCreateService(winsvc.WINDOWS_SERVICE_NAME, "nomad.exe", winsvc.WindowsServiceConfiguration{}, srv, nil)
srv.ExpectEnableEventlog(errors.New("eventlog configure failure"))
},
errOutput: "could not configure eventlog",
status: 1,
},
{
desc: "service stop pre-start failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectCreateService(winsvc.WINDOWS_SERVICE_NAME, "nomad.exe", winsvc.WindowsServiceConfiguration{}, srv, nil)
srv.ExpectEnableEventlog(nil)
srv.ExpectStop(errors.New("service stop failure"))
},
errOutput: "could not stop service",
status: 1,
},
{
desc: "service start failure",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
m.ExpectCreateService(winsvc.WINDOWS_SERVICE_NAME, "nomad.exe", winsvc.WindowsServiceConfiguration{}, srv, nil)
srv.ExpectEnableEventlog(nil)
srv.ExpectStop(nil)
srv.ExpectStart(errors.New("service start failure"))
},
errOutput: "could not start service",
status: 1,
},
{
desc: "upgrade without -reinstall and service running",
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectIsRunning(true, nil)
},
errOutput: "again with -reinstall",
status: 1,
},
{
desc: "upgrade with -reinstall and service running",
args: []string{"-reinstall"},
setup: func(_ string, m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectIsRunning(true, nil)
srv.ExpectStop(nil)
srv.ExpectConfigure(winsvc.WindowsServiceConfiguration{}, nil)
srv.ExpectEnableEventlog(nil)
srv.ExpectStop(nil)
srv.ExpectStart(nil)
},
},
{
desc: "not running as administator",
privilegeFn: func() bool { return false },
errOutput: "must be run with Administator privileges",
status: 1,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
testDir := t.TempDir()
ui := cli.NewMockUi()
mgr := winsvc.NewMockWindowsServiceManager(t)
if tc.setup != nil {
tc.setup(testDir, mgr)
}
pfn := tc.privilegeFn
if pfn == nil {
pfn = func() bool { return true }
}
cmd := &WindowsServiceInstallCommand{
Meta: Meta{Ui: ui},
serviceManagerFn: func() (winsvc.WindowsServiceManager, error) {
return mgr, nil
},
winPaths: createWinPaths(testDir),
privilegedCheckFn: pfn,
}
result := cmd.Run(tc.args)
out := ui.OutputWriter.String()
outErr := ui.ErrorWriter.String()
must.Eq(t, result, tc.status)
if tc.output != "" {
must.StrContains(t, out, tc.output)
}
if tc.errOutput != "" {
must.StrContains(t, outErr, tc.errOutput)
}
if tc.after != nil {
tc.after(testDir)
}
mgr.AssertExpectations()
})
}
}
func createWinPaths(rootDir string) winsvc.WindowsPaths {
return &testWindowsPaths{
SystemRoot: filepath.Join(rootDir, "systemroot"),
SystemDrive: rootDir,
ProgramData: filepath.Join(rootDir, "programdata"),
ProgramFiles: filepath.Join(rootDir, "programfiles"),
}
}
type testWindowsPaths struct {
SystemRoot string
SystemDrive string
ProgramData string
ProgramFiles string
}
func (t *testWindowsPaths) Expand(path string) (string, error) {
tmpl, err := template.New("expansion").Option("missingkey=error").Parse(path)
if err != nil {
return "", err
}
result := new(bytes.Buffer)
if err := tmpl.Execute(result, t); err != nil {
return "", err
}
return strings.ReplaceAll(result.String(), `\`, "/"), nil
}
func (t *testWindowsPaths) CreateDirectory(path string, _ bool) error {
return os.MkdirAll(path, 0o755)
}

View File

@@ -0,0 +1,121 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"fmt"
"strings"
"github.com/hashicorp/nomad/helper/winsvc"
"github.com/posener/complete"
)
type WindowsServiceUninstallCommand struct {
Meta
serviceManagerFn func() (winsvc.WindowsServiceManager, error)
privilegedCheckFn func() bool
}
func (c *WindowsServiceUninstallCommand) AutoCompleteFlags() complete.Flags {
return c.Meta.AutocompleteFlags(FlagSetDefault)
}
func (c *WindowsServiceUninstallCommand) Synopsis() string {
return "Uninstall the nomad Windows system service"
}
func (c *WindowsServiceUninstallCommand) Name() string { return "windows service uninstall" }
func (c *WindowsServiceUninstallCommand) Help() string {
helpText := `
Usage: nomad windows service uninstall [options]
This command uninstalls nomad as a Windows system service.
General Options:
` + generalOptionsUsage(usageOptsDefault)
return strings.TrimSpace(helpText)
}
func (c *WindowsServiceUninstallCommand) Run(args []string) int {
flags := c.Meta.FlagSet(c.Name(), FlagSetDefault)
flags.Usage = func() { c.Ui.Output(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
if args = flags.Args(); len(args) > 0 {
c.Ui.Error("This command takes no arguments")
c.Ui.Error(commandErrorText(c))
return 1
}
// Set helper functions to default if unset
if c.serviceManagerFn == nil {
c.serviceManagerFn = winsvc.NewWindowsServiceManager
}
if c.privilegedCheckFn == nil {
c.privilegedCheckFn = winsvc.IsPrivilegedProcess
}
// Check that command is being run with elevated permissions
if !c.privilegedCheckFn() {
c.Ui.Error("Service uninstall must be run with Administator privileges")
return 1
}
c.Ui.Output("Uninstalling nomad Windows service...")
m, err := c.serviceManagerFn()
if err != nil {
c.Ui.Error(fmt.Sprintf("Could not connect to Windows service manager - %s", err))
return 1
}
defer m.Close()
if err := c.performUninstall(m); err != nil {
c.Ui.Error(fmt.Sprintf("Service uninstall failed: %s", err))
return 1
}
c.Ui.Info("Successfully uninstalled nomad Windows service")
return 0
}
func (c *WindowsServiceUninstallCommand) performUninstall(m winsvc.WindowsServiceManager) error {
// Check that the nomad service is currently registered
exists, err := m.IsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("unable to check for existing service - %w", err)
}
if !exists {
return nil
}
// Grab the service and ensure the service is stopped
srvc, err := m.GetService(winsvc.WINDOWS_SERVICE_NAME)
if err != nil {
return fmt.Errorf("could not get existing service - %w", err)
}
defer srvc.Close()
if err := srvc.Stop(); err != nil {
return fmt.Errorf("unable to stop service - %w", err)
}
// Remove the service from the event log
if err := srvc.DisableEventlog(); err != nil {
return fmt.Errorf("could not remove eventlog configuration - %w", err)
}
// Finally, delete the service
if err := srvc.Delete(); err != nil {
return fmt.Errorf("could not delete service - %w", err)
}
return nil
}

View File

@@ -0,0 +1,146 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"errors"
"testing"
"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/helper/winsvc"
"github.com/shoenig/test/must"
)
func TestWindowsServiceUninstallCommand_Run(t *testing.T) {
testCases := []struct {
desc string
args []string
privilegeFn func() bool
setup func(*winsvc.MockWindowsServiceManager)
output string
errOutput string
status int
}{
{
desc: "service installed",
setup: func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectStop(nil)
srv.ExpectDisableEventlog(nil)
srv.ExpectDelete(nil)
},
output: "uninstalled nomad",
},
{
desc: "service not installed",
setup: func(m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, nil)
},
output: "uninstalled nomad",
},
{
desc: "service registration check failure",
setup: func(m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, false, errors.New("registered check failure"))
},
errOutput: "unable to check for existing service",
status: 1,
},
{
desc: "get service failure",
setup: func(m *winsvc.MockWindowsServiceManager) {
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, nil, errors.New("get service failure"))
},
errOutput: "could not get existing service",
status: 1,
},
{
desc: "service stop failure",
setup: func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectStop(errors.New("service stop failure"))
},
errOutput: "unable to stop service",
status: 1,
},
{
desc: "disable eventlog failure",
setup: func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectStop(nil)
srv.ExpectDisableEventlog(errors.New("disable eventlog failure"))
},
errOutput: "could not remove eventlog configuration",
status: 1,
},
{
desc: "delete service failure",
setup: func(m *winsvc.MockWindowsServiceManager) {
srv := m.NewMockWindowsService()
m.ExpectIsServiceRegistered(winsvc.WINDOWS_SERVICE_NAME, true, nil)
m.ExpectGetService(winsvc.WINDOWS_SERVICE_NAME, srv, nil)
srv.ExpectStop(nil)
srv.ExpectDisableEventlog(nil)
srv.ExpectDelete(errors.New("service delete failure"))
},
errOutput: "could not delete service",
status: 1,
},
{
desc: "with arguments",
args: []string{"any", "value"},
errOutput: "command takes no arguments",
status: 1,
},
{
desc: "not running as administator",
privilegeFn: func() bool { return false },
errOutput: "must be run with Administator privileges",
status: 1,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
ui := cli.NewMockUi()
mgr := winsvc.NewMockWindowsServiceManager(t)
if tc.setup != nil {
tc.setup(mgr)
}
pfn := tc.privilegeFn
if pfn == nil {
pfn = func() bool { return true }
}
cmd := &WindowsServiceUninstallCommand{
Meta: Meta{Ui: ui},
serviceManagerFn: func() (winsvc.WindowsServiceManager, error) {
return mgr, nil
},
privilegedCheckFn: pfn,
}
result := cmd.Run(tc.args)
out := ui.OutputWriter.String()
outErr := ui.ErrorWriter.String()
must.Eq(t, result, tc.status)
if tc.output != "" {
must.StrContains(t, out, tc.output)
}
if tc.errOutput != "" {
must.StrContains(t, outErr, tc.errOutput)
}
mgr.AssertExpectations()
})
}
}

73
helper/winsvc/event.go Normal file
View File

@@ -0,0 +1,73 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
type WindowsEventId uint32
//go:generate stringer -trimprefix=Event -output strings_eventid.go -linecomment -type=WindowsEventId
const (
EventUnknown WindowsEventId = iota // unknown event
EventServiceStarting // service starting
EventServiceReady // service ready
EventServiceStopped // service stopped
EventLogMessage // log message
)
// NewEvent creates a new Event for the Windows Eventlog
func NewEvent(kind WindowsEventId, opts ...EventOption) Event {
evt := &event{
kind: kind,
level: EVENTLOG_LEVEL_INFO,
}
for _, fn := range opts {
fn(evt)
}
return evt
}
type Event interface {
Kind() WindowsEventId
Message() string
Level() EventlogLevel
}
type EventOption func(*event)
// WithEventMessage sets a custom message for the event
func WithEventMessage(msg string) EventOption {
return func(e *event) {
e.message = msg
}
}
// WithEventLevel specifies the level used for the event
func WithEventLevel(level EventlogLevel) EventOption {
return func(e *event) {
e.level = level
}
}
type event struct {
kind WindowsEventId
message string
level EventlogLevel
}
func (e *event) Kind() WindowsEventId {
return e.kind
}
func (e *event) Message() string {
if e.message != "" {
return e.message
}
return e.kind.String()
}
func (e *event) Level() EventlogLevel {
return e.level
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"regexp"
"strings"
)
type EventlogLevel uint8
//go:generate stringer -trimprefix=EVENTLOG_LEVEL_ -output strings_event_logger.go -linecomment -type=EventlogLevel
const (
EVENTLOG_LEVEL_UNKNOWN EventlogLevel = iota
EVENTLOG_LEVEL_INFO
EVENTLOG_LEVEL_WARN
EVENTLOG_LEVEL_ERROR
)
// EventlogLevelFromString converts a log level string to the correct constant
func EventlogLevelFromString(level string) EventlogLevel {
switch strings.ToUpper(level) {
case EVENTLOG_LEVEL_INFO.String():
return EVENTLOG_LEVEL_INFO
case EVENTLOG_LEVEL_WARN.String():
return EVENTLOG_LEVEL_WARN
case EVENTLOG_LEVEL_ERROR.String():
return EVENTLOG_LEVEL_ERROR
}
return EVENTLOG_LEVEL_UNKNOWN
}
var logPattern = regexp.MustCompile(`(?s)\[(ERROR|WARN|INFO)\] (.+)`)
type Eventlog interface {
Info(uint32, string) error
Warning(uint32, string) error
Error(uint32, string) error
Close() error
}
type eventLogger struct {
evtLog Eventlog
level EventlogLevel
}
// Close closes the eventlog
func (e *eventLogger) Close() error {
return e.evtLog.Close()
}
// Write writes logging message to the eventlog
func (e *eventLogger) Write(p []byte) (int, error) {
matches := logPattern.FindStringSubmatch(string(p))
// If no match was found, or the incorrect number of
// elements detected then ignore
if matches == nil || len(matches) != 3 {
return len(p), nil
}
level := EventlogLevelFromString(matches[1])
// If the detected level of the message isn't currently
// allowed then ignore
if !e.allowed(level) {
return len(p), nil
}
// Still here so send the message to the eventlog
switch level {
case EVENTLOG_LEVEL_INFO:
e.evtLog.Info(uint32(EventLogMessage), matches[2])
case EVENTLOG_LEVEL_WARN:
e.evtLog.Warning(uint32(EventLogMessage), matches[2])
case EVENTLOG_LEVEL_ERROR:
e.evtLog.Error(uint32(EventLogMessage), matches[2])
}
return len(p), nil
}
// Check if level is allowed
func (e *eventLogger) allowed(level EventlogLevel) bool {
return level >= e.level
}
type nullEventlog struct{}
func (n *nullEventlog) Info(uint32, string) error {
return nil
}
func (n *nullEventlog) Warning(uint32, string) error {
return nil
}
func (n *nullEventlog) Error(uint32, string) error {
return nil
}
func (n *nullEventlog) Close() error {
return nil
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !windows
package winsvc
import (
"errors"
"io"
)
// NewEventLogger is a stub for non-Windows platforms to generate
// and error when used.
func NewEventLogger(_ string) (io.WriteCloser, error) {
return nil, errors.New("EventLogger is not supported on this platform")
}

View File

@@ -0,0 +1,114 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"io"
"testing"
"github.com/shoenig/test/must"
)
func testEventLogger(e Eventlog, l EventlogLevel) io.WriteCloser {
return &eventLogger{
level: l,
evtLog: e,
}
}
func TestEventlogLevelFromString(t *testing.T) {
t.Run("INFO", func(t *testing.T) {
for _, val := range []string{"INFO", "info"} {
l := EventlogLevelFromString(val)
must.Eq(t, EVENTLOG_LEVEL_INFO, l)
}
})
t.Run("WARN", func(t *testing.T) {
for _, val := range []string{"WARN", "warn"} {
l := EventlogLevelFromString(val)
must.Eq(t, EVENTLOG_LEVEL_WARN, l)
}
})
t.Run("ERROR", func(t *testing.T) {
for _, val := range []string{"ERROR", "error"} {
l := EventlogLevelFromString(val)
must.Eq(t, EVENTLOG_LEVEL_ERROR, l)
}
})
}
func TestEventLogger(t *testing.T) {
defaultmsgs := []string{
"1970-01-01T16:27:16.116Z [INFO] Information line",
"1970-01-01T16:27:16.116Z [WARN] Warning line",
"1970-01-01T16:27:16.116Z [ERROR] Error line",
}
testCases := []struct {
desc string
msgs []string
level EventlogLevel
setup func(*MockEventlog)
}{
{
desc: "basic usage",
level: EVENTLOG_LEVEL_INFO,
setup: func(m *MockEventlog) {
m.ExpectInfo(EventLogMessage, "Information line")
m.ExpectWarning(EventLogMessage, "Warning line")
m.ExpectError(EventLogMessage, "Error line")
},
},
{
desc: "higher level",
level: EVENTLOG_LEVEL_ERROR,
setup: func(m *MockEventlog) {
m.ExpectError(EventLogMessage, "Error line")
},
},
{
desc: "debug and trace logs",
level: EVENTLOG_LEVEL_INFO,
setup: func(m *MockEventlog) {
m.ExpectInfo(EventLogMessage, "Information line")
m.ExpectWarning(EventLogMessage, "Warning line")
m.ExpectError(EventLogMessage, "Error line")
},
msgs: append(defaultmsgs, []string{
"[DEBUG] Debug line",
"[TRACE] Trace line",
}...),
},
{
desc: "with multi-line logs",
level: EVENTLOG_LEVEL_INFO,
setup: func(m *MockEventlog) {
m.ExpectInfo(EventLogMessage, "Information line")
m.ExpectWarning(EventLogMessage, "Warning line")
m.ExpectError(EventLogMessage, "Error line")
m.ExpectInfo(EventLogMessage, "Information log\nthat includes\nmultiple lines")
m.ExpectWarning(EventLogMessage, "Warning log\nthat includes second line")
},
msgs: append(defaultmsgs, []string{
"[INFO] Information log\nthat includes\nmultiple lines",
"[WARN] Warning log\nthat includes second line",
}...),
},
}
for _, tc := range testCases {
if len(tc.msgs) < 1 {
tc.msgs = defaultmsgs
}
el := NewMockEventlog(t)
tc.setup(el)
eventLogger := testEventLogger(el, tc.level)
for _, msg := range tc.msgs {
eventLogger.Write([]byte(msg))
}
el.AssertExpectations()
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"fmt"
"io"
"golang.org/x/sys/windows/svc/eventlog"
)
// NewEventLogger creates a new event logger instance
func NewEventLogger(level string) (io.WriteCloser, error) {
evtLog, err := eventlog.Open(WINDOWS_SERVICE_NAME)
if err != nil {
return nil, fmt.Errorf("Failed to open Windows eventlog: %w", err)
}
return &eventLogger{
evtLog: evtLog,
level: EventlogLevelFromString(level),
}, nil
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"testing"
"github.com/shoenig/test/must"
)
func TestNewEvent(t *testing.T) {
t.Run("simple", func(t *testing.T) {
event := NewEvent(EventServiceReady)
must.Eq(t, EventServiceReady, event.Kind())
must.Eq(t, EVENTLOG_LEVEL_INFO, event.Level())
must.Eq(t, EventServiceReady.String(), event.Message())
})
t.Run("WithEventMessage", func(t *testing.T) {
event := NewEvent(EventServiceReady, WithEventMessage("Custom service ready message"))
must.Eq(t, EventServiceReady, event.Kind())
must.Eq(t, EVENTLOG_LEVEL_INFO, event.Level())
must.Eq(t, "Custom service ready message", event.Message())
})
t.Run("WithEventLevel", func(t *testing.T) {
event := NewEvent(EventServiceReady, WithEventLevel(EVENTLOG_LEVEL_ERROR))
must.Eq(t, EventServiceReady, event.Kind())
must.Eq(t, EVENTLOG_LEVEL_ERROR, event.Level())
must.Eq(t, EventServiceReady.String(), event.Message())
})
t.Run("multiple options", func(t *testing.T) {
event := NewEvent(EventServiceStopped,
WithEventMessage("Custom service stopped message"),
WithEventLevel(EVENTLOG_LEVEL_WARN),
)
must.Eq(t, EventServiceStopped, event.Kind())
must.Eq(t, EVENTLOG_LEVEL_WARN, event.Level())
must.Eq(t, "Custom service stopped message", event.Message())
})
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !windows
package winsvc
// SendEvent sends an event to the Windows eventlog
func SendEvent(e Event) {}

View File

@@ -0,0 +1,26 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"time"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/helper"
)
var chanEvents = make(chan Event)
// SendEvent sends an event to the Windows eventlog
func SendEvent(e Event) {
timer, stop := helper.NewSafeTimer(100 * time.Millisecond)
defer stop()
select {
case chanEvents <- e:
case <-timer.C:
hclog.L().Error("failed to send event to windows eventlog, timed out",
"event", e)
}
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"testing"
"github.com/shoenig/test/must"
)
func NewMockEventlog(t *testing.T) *MockEventlog {
return &MockEventlog{
t: t,
}
}
type MockEventlog struct {
infos []mockArgs
warnings []mockArgs
errors []mockArgs
t *testing.T
}
type mockArgs struct {
winId uint32
msg string
}
func (m *MockEventlog) ExpectInfo(v1 WindowsEventId, v2 string) {
m.infos = append(m.infos, mockArgs{uint32(v1), v2})
}
func (m *MockEventlog) ExpectWarning(v1 WindowsEventId, v2 string) {
m.warnings = append(m.warnings, mockArgs{uint32(v1), v2})
}
func (m *MockEventlog) ExpectError(v1 WindowsEventId, v2 string) {
m.errors = append(m.errors, mockArgs{uint32(v1), v2})
}
func (m *MockEventlog) Info(v1 uint32, v2 string) error {
m.t.Helper()
expectedArgs := m.infos[0]
m.infos = m.infos[1:]
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
return nil
}
func (m *MockEventlog) Warning(v1 uint32, v2 string) error {
m.t.Helper()
expectedArgs := m.warnings[0]
m.warnings = m.warnings[1:]
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
return nil
}
func (m *MockEventlog) Error(v1 uint32, v2 string) error {
m.t.Helper()
expectedArgs := m.errors[0]
m.errors = m.errors[1:]
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
return nil
}
func (m *MockEventlog) Close() error { return nil }
func (m *MockEventlog) AssertExpectations() {
must.SliceEmpty(m.t, m.infos, must.Sprintf("Info expecting %d more invocations", len(m.infos)))
must.SliceEmpty(m.t, m.warnings, must.Sprintf("Warning expecting %d more invocations", len(m.warnings)))
must.SliceEmpty(m.t, m.errors, must.Sprintf("Error expecting %d more invocations", len(m.errors)))
}

View File

@@ -0,0 +1,320 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"reflect"
"testing"
"github.com/shoenig/test/must"
)
func NewMockWindowsServiceManager(t *testing.T) *MockWindowsServiceManager {
t.Helper()
m := &MockWindowsServiceManager{t: t}
return m
}
func NewMockWindowsService(t *testing.T) *MockWindowsService {
t.Helper()
m := &MockWindowsService{t: t}
return m
}
type MockWindowsServiceManager struct {
services []*MockWindowsService
isServiceRegistereds []isServiceRegistered
getServices []getService
createServices []createService
t *testing.T
}
type isServiceRegistered struct {
name string
result bool
err error
}
type getService struct {
name string
result WindowsService
err error
}
type createService struct {
name, binaryPath string
config WindowsServiceConfiguration
result WindowsService
err error
}
func (m *MockWindowsServiceManager) NewMockWindowsService() *MockWindowsService {
w := NewMockWindowsService(m.t)
m.services = append(m.services, w)
return w
}
func (m *MockWindowsServiceManager) ExpectIsServiceRegistered(name string, result bool, err error) {
m.isServiceRegistereds = append(m.isServiceRegistereds, isServiceRegistered{name, result, err})
}
func (m *MockWindowsServiceManager) ExpectGetService(name string, result WindowsService, err error) {
m.getServices = append(m.getServices, getService{name, result, err})
}
func (m *MockWindowsServiceManager) ExpectCreateService(name, binaryPath string, config WindowsServiceConfiguration, result WindowsService, err error) {
m.createServices = append(m.createServices, createService{name, binaryPath, config, result, err})
}
func (m *MockWindowsServiceManager) IsServiceRegistered(name string) (bool, error) {
m.t.Helper()
must.SliceNotEmpty(m.t, m.isServiceRegistereds,
must.Sprint("Unexpected call to IsServiceRegistered"))
call := m.isServiceRegistereds[0]
m.isServiceRegistereds = m.isServiceRegistereds[1:]
must.Eq(m.t, call.name, name,
must.Sprint("IsServiceRegistered received incorrect argument"))
return call.result, call.err
}
func (m *MockWindowsServiceManager) GetService(name string) (WindowsService, error) {
m.t.Helper()
must.SliceNotEmpty(m.t, m.getServices,
must.Sprint("Unexpected call to GetService"))
call := m.getServices[0]
m.getServices = m.getServices[1:]
must.Eq(m.t, call.name, name,
must.Sprint("GetService received incorrect argument"))
return call.result, call.err
}
func (m *MockWindowsServiceManager) CreateService(name, binaryPath string, config WindowsServiceConfiguration) (WindowsService, error) {
m.t.Helper()
must.SliceNotEmpty(m.t, m.createServices,
must.Sprint("Unexpected call to CreateService"))
call := m.createServices[0]
m.createServices = m.createServices[1:]
must.Eq(m.t, call.name, name,
must.Sprint("CreateService received incorrect argument"))
must.StrContains(m.t, binaryPath, call.binaryPath,
must.Sprint("CreateService received incorrect argument"))
if !reflect.ValueOf(call.config).IsZero() {
must.Eq(m.t, call.config, config,
must.Sprint("CreateService received incorrect argument"))
}
return call.result, call.err
}
func (m *MockWindowsServiceManager) Close() error { return nil }
func (m *MockWindowsServiceManager) AssertExpectations() {
m.t.Helper()
must.SliceEmpty(m.t, m.isServiceRegistereds,
must.Sprintf("IsServiceRegistered expecting %d more invocations", len(m.isServiceRegistereds)))
must.SliceEmpty(m.t, m.getServices,
must.Sprintf("GetService expecting %d more invocations", len(m.getServices)))
must.SliceEmpty(m.t, m.createServices,
must.Sprintf("CreateService expecting %d more invocations", len(m.createServices)))
for _, srv := range m.services {
srv.AssertExpectations()
}
}
type MockWindowsService struct {
names []string
configures []configure
starts []error
stops []error
deletes []error
isRunnings []iscall
isStoppeds []iscall
enableEventlogs []error
disableEventlogs []error
t *testing.T
}
type configure struct {
config WindowsServiceConfiguration
err error
}
type iscall struct {
result bool
err error
}
func (m *MockWindowsService) ExpectName(result string) {
m.names = append(m.names, result)
}
func (m *MockWindowsService) ExpectConfigure(config WindowsServiceConfiguration, err error) {
m.configures = append(m.configures, configure{config, err})
}
func (m *MockWindowsService) ExpectStart(err error) {
m.starts = append(m.starts, err)
}
func (m *MockWindowsService) ExpectStop(err error) {
m.stops = append(m.stops, err)
}
func (m *MockWindowsService) ExpectDelete(err error) {
m.deletes = append(m.deletes, err)
}
func (m *MockWindowsService) ExpectIsRunning(result bool, err error) {
m.isRunnings = append(m.isRunnings, iscall{result, err})
}
func (m *MockWindowsService) ExpectIsStopped(result bool, err error) {
m.isStoppeds = append(m.isStoppeds, iscall{result, err})
}
func (m *MockWindowsService) ExpectEnableEventlog(err error) {
m.enableEventlogs = append(m.enableEventlogs, err)
}
func (m *MockWindowsService) ExpectDisableEventlog(err error) {
m.disableEventlogs = append(m.disableEventlogs, err)
}
func (m *MockWindowsService) Name() string {
m.t.Helper()
must.SliceNotEmpty(m.t, m.names,
must.Sprint("Unexpected call to Name"))
name := m.names[0]
m.names = m.names[1:]
return name
}
func (m *MockWindowsService) Configure(config WindowsServiceConfiguration) error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.configures,
must.Sprint("Unexpected call to Configure"))
call := m.configures[0]
m.configures = m.configures[1:]
if !reflect.ValueOf(call.config).IsZero() {
must.Eq(m.t, call.config, config,
must.Sprint("Configure received incorrect argument"))
}
return call.err
}
func (m *MockWindowsService) Start() error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.starts,
must.Sprint("Unexpected call to Start"))
err := m.starts[0]
m.starts = m.starts[1:]
return err
}
func (m *MockWindowsService) Stop() error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.stops,
must.Sprint("Unexpected call to Stop"))
err := m.stops[0]
m.stops = m.stops[1:]
return err
}
func (m *MockWindowsService) Delete() error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.deletes,
must.Sprint("Unexpected call to Delete"))
err := m.deletes[0]
m.deletes = m.deletes[1:]
return err
}
func (m *MockWindowsService) IsRunning() (bool, error) {
m.t.Helper()
must.SliceNotEmpty(m.t, m.isRunnings,
must.Sprint("Unexpected call to IsRunning"))
call := m.isRunnings[0]
m.isRunnings = m.isRunnings[1:]
return call.result, call.err
}
func (m *MockWindowsService) IsStopped() (bool, error) {
m.t.Helper()
must.SliceNotEmpty(m.t, m.isStoppeds,
must.Sprint("Unexpected call to IsStopped"))
call := m.isStoppeds[0]
m.isStoppeds = m.isStoppeds[1:]
return call.result, call.err
}
func (m *MockWindowsService) EnableEventlog() error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.enableEventlogs,
must.Sprint("Unexpected call to EnableEventlog"))
err := m.enableEventlogs[0]
m.enableEventlogs = m.enableEventlogs[1:]
return err
}
func (m *MockWindowsService) DisableEventlog() error {
m.t.Helper()
must.SliceNotEmpty(m.t, m.disableEventlogs,
must.Sprint("Unexpected call to DisableEventlog"))
err := m.disableEventlogs[0]
m.disableEventlogs = m.disableEventlogs[1:]
return err
}
func (m *MockWindowsService) Close() error { return nil }
func (m *MockWindowsService) AssertExpectations() {
m.t.Helper()
must.SliceEmpty(m.t, m.names,
must.Sprintf("Name expecting %d more invocations", len(m.names)))
must.SliceEmpty(m.t, m.configures,
must.Sprintf("Configure expecting %d more invocations", len(m.configures)))
must.SliceEmpty(m.t, m.starts,
must.Sprintf("Start expecting %d more invocations", len(m.starts)))
must.SliceEmpty(m.t, m.stops,
must.Sprintf("Stop expecting %d more invocations", len(m.stops)))
must.SliceEmpty(m.t, m.deletes,
must.Sprintf("Delete expecting %d more invocations", len(m.deletes)))
must.SliceEmpty(m.t, m.isRunnings,
must.Sprintf("IsRunning expecting %d more invocations", len(m.isRunnings)))
must.SliceEmpty(m.t, m.isStoppeds,
must.Sprintf("IsStopped expecting %d more invocations", len(m.isStoppeds)))
must.SliceEmpty(m.t, m.enableEventlogs,
must.Sprintf("EnableEventlog expecting %d more invocations", len(m.enableEventlogs)))
must.SliceEmpty(m.t, m.disableEventlogs,
must.Sprintf("DisableEventlog expecting %d more invocations", len(m.disableEventlogs)))
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !windows
package winsvc
import "errors"
func NewWindowsPaths() WindowsPaths {
return &windowsPaths{}
}
type windowsPaths struct{}
func (w *windowsPaths) Expand(string) (string, error) {
return "", errors.New("Windows path expansion not supported on this platform")
}
func (w *windowsPaths) CreateDirectory(string, bool) error {
return errors.New("Windows directory creation not supported on this platform")
}

View File

@@ -0,0 +1,206 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"bytes"
"errors"
"fmt"
"os"
"strings"
"sync"
"text/template"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
func NewWindowsPaths() WindowsPaths {
return &windowsPaths{}
}
type windowsPaths struct {
SystemRoot string
SystemDrive string
ProgramData string
ProgramFiles string
loadErr error
o sync.Once
}
func (w *windowsPaths) Expand(path string) (string, error) {
if err := w.load(); err != nil {
return "", err
}
tmpl := template.New("expansion").Option("missingkey=error")
tmpl, err := tmpl.Parse(path)
if err != nil {
return "", err
}
result := new(bytes.Buffer)
if err := tmpl.Execute(result, w); err != nil {
return "", err
}
return result.String(), nil
}
func (w *windowsPaths) CreateDirectory(path string, restrict_on_create bool) error {
s, err := os.Stat(path)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if err == nil {
// Directory exists so nothing to do
if s.IsDir() {
return nil
}
return fmt.Errorf("path exists and is not directory - %s", path)
}
// NOTE: mode ignored on Windows. If directory should
// be restricted, an ACL will be applied below.
if err := os.MkdirAll(path, 0o000); err != nil {
return err
}
// Since the directory was just created, apply access
// restrictions if requested
if restrict_on_create {
if err := setDirectoryPermissions(path); err != nil {
return err
}
}
return nil
}
func getUserGroupSIDs() (usid *windows.SID, gsid *windows.SID, err error) {
// NOTE: this token is a pseudo-token and does not
// need to be closed
token := windows.GetCurrentProcessToken()
userToken, err := token.GetTokenUser()
if err != nil {
return
}
usid = userToken.User.Sid
userGroup, err := token.GetTokenPrimaryGroup()
if err != nil {
return
}
gsid = userGroup.PrimaryGroup
return
}
func setDirectoryPermissions(path string) error {
// Grab the user and group SID for who is running the process
userSid, groupSid, err := getUserGroupSIDs()
if err != nil {
return err
}
// Generate a SID for the administators group
gsid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
if err != nil {
return err
}
// Create an ACL with an ACE for user SID and an ACE for the
// administrators group SID, both of which are granted full
// access. No other ACEs are provided which restricts access
// from non-administrators
dacl, err := windows.ACLFromEntries(
[]windows.EXPLICIT_ACCESS{
{
AccessPermissions: windows.GENERIC_ALL,
AccessMode: windows.SET_ACCESS,
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
Trustee: windows.TRUSTEE{
MultipleTrusteeOperation: windows.NO_MULTIPLE_TRUSTEE,
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_USER,
TrusteeValue: windows.TrusteeValueFromSID(userSid),
},
},
{
AccessPermissions: windows.GENERIC_ALL,
AccessMode: windows.SET_ACCESS,
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
Trustee: windows.TRUSTEE{
MultipleTrusteeOperation: windows.NO_MULTIPLE_TRUSTEE,
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_WELL_KNOWN_GROUP,
TrusteeValue: windows.TrusteeValueFromSID(gsid),
},
},
}, nil,
)
if err != nil {
return err
}
// Apply the ACL to the directory
if err := windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT,
windows.OWNER_SECURITY_INFORMATION|
windows.GROUP_SECURITY_INFORMATION|
windows.DACL_SECURITY_INFORMATION|
windows.PROTECTED_DACL_SECURITY_INFORMATION,
userSid, groupSid, dacl, nil); err != nil {
return err
}
return nil
}
func (w *windowsPaths) load() error {
w.o.Do(func() {
w.SystemDrive = os.Getenv("SystemDrive")
if w.SystemDrive == "" {
w.loadErr = fmt.Errorf("cannot detect Windows SystemDrive path")
return
}
w.SystemRoot = strings.ReplaceAll(os.Getenv("SystemDrive"), "SystemDrive", w.SystemDrive)
w.ProgramData = os.Getenv("ProgramData")
if w.ProgramData == "" {
pdKey, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList`, registry.QUERY_VALUE)
if err == nil {
if pdVal, _, err := pdKey.GetStringValue("ProgramData"); err == nil {
w.ProgramData = pdVal
}
}
}
if w.ProgramData == "" {
w.loadErr = fmt.Errorf("cannot detect Windows ProgramData path")
return
}
w.ProgramData = strings.ReplaceAll(w.ProgramData, "SystemDrive", w.SystemDrive)
w.ProgramFiles = os.Getenv("ProgramFiles")
if w.ProgramFiles == "" {
pdKey, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows\CurrentVersion`, registry.QUERY_VALUE)
if err == nil {
if pdVal, _, err := pdKey.GetStringValue("ProgramFilesDir"); err == nil {
w.ProgramFiles = pdVal
}
}
}
if w.ProgramFiles == "" {
w.loadErr = fmt.Errorf("cannot detect Windows ProgramFiles path")
return
}
w.ProgramFiles = strings.ReplaceAll(w.ProgramFiles, "SystemDrive", w.SystemDrive)
})
return w.loadErr
}

View File

@@ -0,0 +1,203 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"os"
"path/filepath"
"testing"
"unsafe"
"github.com/hashicorp/nomad/ci"
"github.com/shoenig/test/must"
"golang.org/x/sys/windows"
)
func TestCreateDirectory(t *testing.T) {
ci.Parallel(t)
testDir := t.TempDir()
t.Run("create", func(t *testing.T) {
// NOTE: parallel is not set here to force parent
// to wait for subtests to complete
t.Run("unrestricted", func(t *testing.T) {
ci.Parallel(t)
path := filepath.Join(testDir, t.Name())
err := NewWindowsPaths().CreateDirectory(path, false)
must.NoError(t, err)
dacl := getDirectoryDACL(t, path)
// When not applying restrictions on the new directory, all
// ACEs will be inherited from the parent
for i := range dacl.AceCount {
ace := &windows.ACCESS_ALLOWED_ACE{}
must.NoError(t, windows.GetAce(dacl, uint32(i), &ace), must.Sprint("failed to load ACE"))
must.Eq(t, windows.INHERITED_ACCESS_ENTRY, ace.Header.AceFlags&windows.INHERITED_ACCESS_ENTRY,
must.Sprint("ACE is not inherited"))
}
})
t.Run("restricted", func(t *testing.T) {
ci.Parallel(t)
path := filepath.Join(testDir, t.Name())
err := NewWindowsPaths().CreateDirectory(path, true)
must.NoError(t, err)
dacl := getDirectoryDACL(t, path)
matches := map[string]struct{}{}
// When restrictions are applied on the new directory, all
// ACEs will be directly applied.
for i := range dacl.AceCount {
ace := &windows.ACCESS_ALLOWED_ACE{}
must.NoError(t, windows.GetAce(dacl, uint32(i), &ace), must.Sprint("failed to load ACE"))
must.NotEq(t, windows.INHERITED_ACCESS_ENTRY, ace.Header.AceFlags&windows.INHERITED_ACCESS_ENTRY,
must.Sprint("ACE should not be inherited"))
if ace.Mask&windows.GENERIC_ALL == windows.GENERIC_ALL {
sid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))
matches[sid.String()] = struct{}{}
}
}
// All privileges should be set for user and administrators groups
adminGroupSID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
must.NoError(t, err, must.Sprint("failed to create well known administrators group SID"))
userSID, _, err := getUserGroupSIDs()
must.NoError(t, err, must.Sprint("failed to get user SID"))
must.NotNil(t, matches[userSID.String()], must.Sprint("missing user ACE with GENERIC_ALL"))
must.NotNil(t, matches[adminGroupSID.String()],
must.Sprint("missing administrators group ACE with GENERIC_ALL"))
must.Eq(t, 2, len(matches), must.Sprint("unexpected GENERIC_ALL ACEs found"))
})
t.Run("unrestricted already exists", func(t *testing.T) {
ci.Parallel(t)
path := filepath.Join(testDir, t.Name())
must.NoError(t, os.MkdirAll(path, 0o000))
err := NewWindowsPaths().CreateDirectory(path, false)
must.NoError(t, err)
dacl := getDirectoryDACL(t, path)
// No restrictions are applied, so check that all ACEs
// are inherited from parent
for i := range dacl.AceCount {
ace := &windows.ACCESS_ALLOWED_ACE{}
must.NoError(t, windows.GetAce(dacl, uint32(i), &ace), must.Sprint("failed to load ACE"))
must.Eq(t, windows.INHERITED_ACCESS_ENTRY, ace.Header.AceFlags&windows.INHERITED_ACCESS_ENTRY,
must.Sprint("ACE is not inherited"))
}
})
t.Run("restricted already exists", func(t *testing.T) {
ci.Parallel(t)
path := filepath.Join(testDir, t.Name())
must.NoError(t, os.MkdirAll(path, 0o000))
err := NewWindowsPaths().CreateDirectory(path, true)
must.NoError(t, err)
dacl := getDirectoryDACL(t, path)
// When the directory already exists, restrictions should not
// be applied so validate that all ACEs are inherited
for i := range dacl.AceCount {
ace := &windows.ACCESS_ALLOWED_ACE{}
must.NoError(t, windows.GetAce(dacl, uint32(i), &ace), must.Sprint("failed to load ACE"))
must.Eq(t, windows.INHERITED_ACCESS_ENTRY, ace.Header.AceFlags&windows.INHERITED_ACCESS_ENTRY,
must.Sprint("ACE is not inherited"))
}
})
})
}
func TestExpand(t *testing.T) {
t.Run("SystemDrive", func(t *testing.T) {
t.Run("default", func(t *testing.T) {
result, err := NewWindowsPaths().Expand(`{{.SystemDrive}}/testing`)
must.NoError(t, err)
must.StrNotContains(t, result, "{{.SystemDrive}}")
})
t.Run("custom environment variable", func(t *testing.T) {
t.Setenv("SystemDrive", `z:`)
result, err := NewWindowsPaths().Expand(`{{.SystemDrive}}\testing`)
must.NoError(t, err)
must.Eq(t, `z:\testing`, result)
})
t.Run("unset environment variable", func(t *testing.T) {
t.Setenv("SystemDrive", "")
_, err := NewWindowsPaths().Expand(`{{.SystemDrive}}\testing`)
must.ErrorContains(t, err, "cannot detect Windows SystemDrive path")
})
})
t.Run("ProgramData", func(t *testing.T) {
t.Run("default", func(t *testing.T) {
result, err := NewWindowsPaths().Expand(`{{.ProgramData}}/testing`)
must.NoError(t, err)
must.StrNotContains(t, result, "{{.ProgramData}}")
})
t.Run("custom environment variable", func(t *testing.T) {
t.Setenv("ProgramData", `z:`)
result, err := NewWindowsPaths().Expand(`{{.ProgramData}}\testing`)
must.NoError(t, err)
must.Eq(t, `z:\testing`, result)
})
t.Run("unset environment variable", func(t *testing.T) {
t.Setenv("ProgramData", "")
result, err := NewWindowsPaths().Expand(`{{.ProgramData}}\testing`)
must.NoError(t, err)
must.StrNotContains(t, result, "{{.ProgramData}}") // should be pulled from registry
})
})
t.Run("ProgramFiles", func(t *testing.T) {
t.Run("default", func(t *testing.T) {
result, err := NewWindowsPaths().Expand(`{{.ProgramFiles}}/testing`)
must.NoError(t, err)
must.StrNotContains(t, result, "{{.ProgramFiles}}")
})
t.Run("custom environment variable", func(t *testing.T) {
t.Setenv("ProgramFiles", `z:`)
result, err := NewWindowsPaths().Expand(`{{.ProgramFiles}}\testing`)
must.NoError(t, err)
must.Eq(t, `z:\testing`, result)
})
t.Run("unset environment variable", func(t *testing.T) {
t.Setenv("ProgramFiles", "")
result, err := NewWindowsPaths().Expand(`{{.ProgramFiles}}\testing`)
must.NoError(t, err)
must.StrNotContains(t, result, "{{.ProgramFiles}}") // should be pulled from registry
})
})
t.Run("missing key", func(t *testing.T) {
_, err := NewWindowsPaths().Expand(`{{.Unknown}}\testing`)
must.ErrorContains(t, err, "can't evaluate field")
})
}
func getDirectoryDACL(t *testing.T, path string) *windows.ACL {
t.Helper()
s, err := os.Stat(path)
must.NoError(t, err)
must.True(t, s.IsDir(), must.Sprint("expected path to be a directory"))
info, err := windows.GetNamedSecurityInfo(path,
windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION)
must.NoError(t, err, must.Sprint("failed to get path security information"))
dacl, _, err := info.DACL()
must.NoError(t, err, must.Sprint("failed to get path ACL"))
return dacl
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !windows
package winsvc
// IsPrivilegedProcess checks if current process is a privileged windows process
func IsPrivilegedProcess() bool {
return false
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import "golang.org/x/sys/windows"
// IsPrivilegedProcess checks if current process is a privileged windows process
func IsPrivilegedProcess() bool {
return windows.GetCurrentProcessToken().IsElevated()
}

View File

@@ -3,10 +3,22 @@
package winsvc
var chanGraceExit = make(chan int)
const (
WINDOWS_SERVICE_NAME = "nomad"
WINDOWS_SERVICE_DISPLAY_NAME = "HashiCorp Nomad"
WINDOWS_SERVICE_DESCRIPTION = "Workload scheduler and orchestrator - https://nomadproject.io"
WINDOWS_INSTALL_BIN_DIRECTORY = `{{.ProgramFiles}}\HashiCorp\nomad\bin`
WINDOWS_INSTALL_APPDATA_DIRECTORY = `{{.ProgramData}}\HashiCorp\nomad`
// Number of seconds to wait for a
// service to reach a desired state
WINDOWS_SERVICE_STATE_TIMEOUT = "1m"
)
var chanGraceExit = make(chan struct{})
// ShutdownChannel returns a channel that sends a message that a shutdown
// signal has been received for the service.
func ShutdownChannel() <-chan int {
func ShutdownChannel() <-chan struct{} {
return chanGraceExit
}

View File

@@ -1,46 +1,84 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build windows
// +build windows
package winsvc
import (
wsvc "golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/eventlog"
)
type serviceWindows struct{}
// Commands that are currently supported
const SERVICE_ACCEPTED_COMMANDS = svc.AcceptStop | svc.AcceptShutdown
type serviceWindows struct {
evtLog Eventlog
}
func init() {
interactive, err := wsvc.IsAnInteractiveSession()
isSvc, err := svc.IsWindowsService()
if err != nil {
panic(err)
}
// Cannot run as a service when running interactively
if interactive {
// This should only run when running
// as a service
if !isSvc {
return
}
go wsvc.Run("", serviceWindows{})
go executeWindowsService()
}
// Execute implements the Windows service Handler type. It will be
// called at the start of the service, and the service will exit
// once Execute completes.
func (serviceWindows) Execute(args []string, r <-chan wsvc.ChangeRequest, s chan<- wsvc.Status) (svcSpecificEC bool, exitCode uint32) {
const accCommands = wsvc.AcceptStop | wsvc.AcceptShutdown
s <- wsvc.Status{State: wsvc.Running, Accepts: accCommands}
func (srv serviceWindows) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) {
s <- svc.Status{State: svc.Running, Accepts: SERVICE_ACCEPTED_COMMANDS}
srv.evtLog.Info(uint32(EventServiceStarting), "service starting")
LOOP:
for {
c := <-r
switch c.Cmd {
case wsvc.Interrogate:
s <- c.CurrentStatus
case wsvc.Stop, wsvc.Shutdown:
s <- wsvc.Status{State: wsvc.StopPending}
chanGraceExit <- 1
return false, 0
select {
case c := <-r:
switch c.Cmd {
case svc.Interrogate:
s <- c.CurrentStatus
case svc.Stop, svc.Shutdown:
srv.evtLog.Info(uint32(EventLogMessage), "service stop requested")
s <- svc.Status{State: svc.StopPending}
close(chanGraceExit)
}
case e := <-chanEvents:
switch e.Level() {
case EVENTLOG_LEVEL_INFO:
srv.evtLog.Info(uint32(e.Kind()), e.Message())
case EVENTLOG_LEVEL_WARN:
srv.evtLog.Warning(uint32(e.Kind()), e.Message())
case EVENTLOG_LEVEL_ERROR:
srv.evtLog.Error(uint32(e.Kind()), e.Message())
}
if e.Kind() == EventServiceStopped {
break LOOP
}
}
}
return false, 0
}
func executeWindowsService() {
var evtLog Eventlog
evtLog, err := eventlog.Open(WINDOWS_SERVICE_NAME)
if err != nil {
// Eventlog will only be available if the
// service was properly registered. If the
// service was manually setup, it will likely
// not have been registered with the eventlog
// so it will not be available. In that case
// just stub out the eventlog.
evtLog = &nullEventlog{}
}
defer evtLog.Close()
svc.Run(WINDOWS_SERVICE_NAME, serviceWindows{evtLog: evtLog})
}

View File

@@ -0,0 +1,26 @@
// Code generated by "stringer -trimprefix=EVENTLOG_LEVEL_ -output strings_event_logger.go -linecomment -type=EventlogLevel"; DO NOT EDIT.
package winsvc
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[EVENTLOG_LEVEL_UNKNOWN-0]
_ = x[EVENTLOG_LEVEL_INFO-1]
_ = x[EVENTLOG_LEVEL_WARN-2]
_ = x[EVENTLOG_LEVEL_ERROR-3]
}
const _EventlogLevel_name = "UNKNOWNINFOWARNERROR"
var _EventlogLevel_index = [...]uint8{0, 7, 11, 15, 20}
func (i EventlogLevel) String() string {
if i >= EventlogLevel(len(_EventlogLevel_index)-1) {
return "EventlogLevel(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _EventlogLevel_name[_EventlogLevel_index[i]:_EventlogLevel_index[i+1]]
}

View File

@@ -0,0 +1,27 @@
// Code generated by "stringer -trimprefix=Event -output strings_eventid.go -linecomment -type=WindowsEventId"; DO NOT EDIT.
package winsvc
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[EventUnknown-0]
_ = x[EventServiceStarting-1]
_ = x[EventServiceReady-2]
_ = x[EventServiceStopped-3]
_ = x[EventLogMessage-4]
}
const _WindowsEventId_name = "unknown eventservice startingservice readyservice stoppedlog message"
var _WindowsEventId_index = [...]uint8{0, 13, 29, 42, 57, 68}
func (i WindowsEventId) String() string {
if i >= WindowsEventId(len(_WindowsEventId_index)-1) {
return "WindowsEventId(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _WindowsEventId_name[_WindowsEventId_index[i]:_WindowsEventId_index[i+1]]
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
type ServiceStartType uint32
// extracted from https://pkg.go.dev/golang.org/x/sys@v0.35.0/windows/svc/mgr#StartManual
const (
StartManual ServiceStartType = 3
StartAutomatic ServiceStartType = 2
StartDisabled ServiceStartType = 4
)
type WindowsServiceConfiguration struct {
StartType ServiceStartType
DisplayName string
Description string
BinaryPathName string
}
type WindowsPaths interface {
// Expand expands the path defined by the template. Supports
// values for:
// - SystemDrive
// - SystemRoot
// - ProgramData
// - ProgramFiles
Expand(path string) (string, error)
// Creates a new directory if it does not exist. If directory
// is created and restrict_on_create is true, a restrictive
// ACL is applied.
CreateDirectory(path string, restrict_on_create bool) error
}
type WindowsService interface {
// Name returns the name of the service
Name() string
// Configure applies the configuration to the Windows service.
// NOTE: Full configuration applied so empty values will remove existing values.
Configure(config WindowsServiceConfiguration) error
// Start starts the Windows service and waits for the
// service to be running.
Start() error
// Stop requests the service to stop and waits for the
// service to stop.
Stop() error
// Close closes the connection to the Windows service.
Close() error
// Delete deletes the Windows service.
Delete() error
// IsRunning returns if the service is currently running.
IsRunning() (bool, error)
// IsStopped returns if the service is currently stopped.
IsStopped() (bool, error)
// EnableEventlog will add or update the Windows Eventlog
// configuration for the service. It will set supported
// events as info, warning, and error.
EnableEventlog() error
// DisableEventlog will remove the Windows Eventlog configuration
// for the service.
DisableEventlog() error
}
type WindowsServiceManager interface {
// IsServiceRegistered returns if the service is a registered Windows service.
IsServiceRegistered(name string) (bool, error)
// GetService opens and returns the named service.
GetService(name string) (WindowsService, error)
// CreateService creates a new Windows service.
CreateService(name, binaryPath string, config WindowsServiceConfiguration) (WindowsService, error)
// Close closes Windows service manager connection.
Close() error
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !windows
package winsvc
import (
"errors"
)
// NewWindowsServiceManager returns an error
func NewWindowsServiceManager() (WindowsServiceManager, error) {
return nil, errors.New("Windows service manager is not supported on this platform")
}

View File

@@ -0,0 +1,256 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"os/signal"
"reflect"
"slices"
"time"
"github.com/hashicorp/nomad/helper"
"golang.org/x/sys/windows/registry"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/eventlog"
"golang.org/x/sys/windows/svc/mgr"
)
// Base registry path for eventlog registrations
const EVENTLOG_REGISTRY_PATH = `SYSTEM\CurrentControlSet\Services\EventLog\Application`
// Registry value name for supported event types
const EVENTLOG_SUPPORTED_EVENTS_KEY = "TypesSupported"
// Event types registered as supported
const EVENTLOG_SUPPORTED_EVENTS uint32 = eventlog.Error | eventlog.Warning | eventlog.Info
// NewWindowsServiceManager creates a new instance of the wrapper
// to interact with the Windows service manager.
func NewWindowsServiceManager() (WindowsServiceManager, error) {
m, err := mgr.Connect()
if err != nil {
return nil, err
}
return &windowsServiceManager{manager: m}, nil
}
type windowsServiceManager struct {
manager *mgr.Mgr
}
func (m *windowsServiceManager) IsServiceRegistered(name string) (bool, error) {
list, err := m.manager.ListServices()
if err != nil {
return false, err
}
if slices.Contains(list, name) {
return true, nil
}
return false, nil
}
func (m *windowsServiceManager) GetService(name string) (WindowsService, error) {
service, err := m.manager.OpenService(name)
if err != nil {
return nil, err
}
return &windowsService{service: service}, nil
}
func (m *windowsServiceManager) CreateService(name, bin string, config WindowsServiceConfiguration) (WindowsService, error) {
wsvc, err := m.manager.CreateService(name, bin, mgr.Config{})
if err != nil {
return nil, err
}
service := &windowsService{service: wsvc}
// Only apply configuration if configuration is provided
if !reflect.ValueOf(config).IsZero() {
if err := service.Configure(config); err != nil {
return nil, err
}
}
return service, nil
}
func (m *windowsServiceManager) Close() error {
return m.manager.Disconnect()
}
type windowsService struct {
service *mgr.Service
}
func (s *windowsService) Name() string {
return s.service.Name
}
func (s *windowsService) Configure(config WindowsServiceConfiguration) error {
serviceCfg, err := s.service.Config()
if err != nil {
return err
}
serviceCfg.StartType = uint32(config.StartType)
serviceCfg.DisplayName = config.DisplayName
serviceCfg.Description = config.Description
serviceCfg.BinaryPathName = config.BinaryPathName
if err := s.service.UpdateConfig(serviceCfg); err != nil {
return err
}
return nil
}
func (s *windowsService) Start() error {
if running, _ := s.IsRunning(); running {
return nil
}
if err := s.service.Start(); err != nil {
return err
}
if err := waitFor(context.Background(), s.IsRunning); err != nil {
return err
}
return nil
}
func (s *windowsService) Stop() error {
if stopped, _ := s.IsStopped(); stopped {
return nil
}
if _, err := s.service.Control(svc.Stop); err != nil {
return err
}
if err := waitFor(context.Background(), s.IsStopped); err != nil {
return err
}
return nil
}
func (s *windowsService) Close() error {
return s.service.Close()
}
func (s *windowsService) Delete() error {
return s.service.Delete()
}
func (s *windowsService) IsRunning() (bool, error) {
return s.isService(svc.Running)
}
func (s *windowsService) IsStopped() (bool, error) {
return s.isService(svc.Stopped)
}
func (s *windowsService) EnableEventlog() error {
// Check if the service is already setup in the eventlog
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
EVENTLOG_REGISTRY_PATH+`\`+s.Name(),
registry.ALL_ACCESS,
)
// If it could not be opened, assume error is caused
// due to nonexistence. If it was for some other reason
// the error will be encountered again when attempting to
// create.
if err != nil {
if err := eventlog.InstallAsEventCreate(s.Name(), EVENTLOG_SUPPORTED_EVENTS); err != nil {
return err
}
} else {
defer key.Close()
// Since the service is already registered, just
// ensure it is properly configured. Currently
// that just means the supported events.
val, _, err := key.GetIntegerValue(EVENTLOG_SUPPORTED_EVENTS_KEY)
if err != nil || uint32(val) != EVENTLOG_SUPPORTED_EVENTS {
if err := key.SetDWordValue(EVENTLOG_SUPPORTED_EVENTS_KEY, EVENTLOG_SUPPORTED_EVENTS); err != nil {
return err
}
}
}
return nil
}
func (s *windowsService) DisableEventlog() error {
// Check if the service is currently enabled in the eventlog
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
EVENTLOG_REGISTRY_PATH+`\`+s.Name(),
registry.READ,
)
if errors.Is(err, fs.ErrNotExist) {
return nil
}
defer key.Close()
return eventlog.Remove(s.Name())
}
func (s *windowsService) isService(state svc.State) (bool, error) {
status, err := s.service.Query()
if err != nil {
return false, err
}
return status.State == state, nil
}
func waitFor(ctx context.Context, condition func() (bool, error)) error {
d, err := time.ParseDuration(WINDOWS_SERVICE_STATE_TIMEOUT)
if err != nil {
return err
}
// Setup a deadline
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(d))
defer cancel()
// Watch for any interrupts
ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
defer stop()
pauseDur := time.Millisecond * 250
t, timerStop := helper.NewSafeTimer(pauseDur)
defer timerStop()
for {
t.Reset(pauseDur)
complete, err := condition()
if err != nil {
return err
}
if complete {
return nil
}
select {
case <-ctx.Done():
return fmt.Errorf("timeout exceeded waiting for process")
case <-t.C:
}
}
}

View File

@@ -0,0 +1,598 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"context"
"io/fs"
"testing"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/nomad/ci"
"github.com/shoenig/test/must"
"golang.org/x/sys/windows/registry"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
)
func TestWindowsServiceManager(t *testing.T) {
ci.Parallel(t)
t.Run("IsServiceRegistered", func(t *testing.T) {
ci.Parallel(t)
t.Run("service does not exist", func(t *testing.T) {
ci.Parallel(t)
_, manager := makeManagers(t)
result, err := manager.IsServiceRegistered("fake-service-name")
must.NoError(t, err, must.Sprint("check should not error"))
must.False(t, result, must.Sprint("service should not exist"))
})
t.Run("service does exist", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
result, err := manager.IsServiceRegistered(serviceName)
must.NoError(t, err, must.Sprint("check should not error"))
must.True(t, result, must.Sprint("service should exist"))
})
})
t.Run("GetService", func(t *testing.T) {
ci.Parallel(t)
t.Run("service does not exist", func(t *testing.T) {
ci.Parallel(t)
_, manager := makeManagers(t)
_, err := manager.GetService("fake-service-name")
must.ErrorContains(t, err, "specified service does not exist",
must.Sprint("error should be generated when service does not exist"))
})
t.Run("service does exist", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
srv, err := manager.GetService(serviceName)
must.NoError(t, err)
defer srv.Close()
must.Eq(t, serviceName, srv.Name(), must.Sprint("service name does not match"))
})
})
t.Run("CreateService", func(t *testing.T) {
ci.Parallel(t)
t.Run("service does not exist", func(t *testing.T) {
ci.Parallel(t)
serviceName := generateServiceName()
m, manager := makeManagers(t)
srv, err := manager.CreateService(serviceName, `c:\stub`, WindowsServiceConfiguration{})
must.NoError(t, err)
defer srv.Close()
defer deleteStubService(t, m, serviceName)
must.Eq(t, serviceName, srv.Name(), must.Sprint("new service name is incorrect"))
})
t.Run("service does exist", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
_, err := manager.CreateService(serviceName, `c:\stub`, WindowsServiceConfiguration{})
must.ErrorContains(t, err, "service already exists", must.Sprint("service creation should fail"))
})
t.Run("with configuration", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateServiceName()
srv, err := manager.CreateService(serviceName, `c:\stub`,
WindowsServiceConfiguration{DisplayName: "testing service", StartType: StartDisabled})
must.NoError(t, err, must.Sprint("service should be created"))
defer srv.Close()
defer deleteStubService(t, m, serviceName)
directSrv, err := m.OpenService(serviceName)
must.NoError(t, err, must.Sprint("direct service connection should succeed"))
defer directSrv.Close()
config, err := directSrv.Config()
must.NoError(t, err, must.Sprint("configuration should be available from service"))
must.Eq(t, "testing service", config.DisplayName, must.Sprint("new service name does not match"))
})
})
}
// This is a simple service available in Windows. It will
// be used to locate the executable so a test service can
// be created using it that will allow proper start/stop
// testing.
const TEST_WINDOWS_SERVICE = "SNMPTrap"
func TestWindowsService(t *testing.T) {
ci.Parallel(t)
mg, _ := makeManagers(t)
snmpSvc, err := mg.OpenService(TEST_WINDOWS_SERVICE)
must.NoError(t, err)
defer snmpSvc.Close()
snmpConfig, err := snmpSvc.Config()
must.NoError(t, err)
binPath := snmpConfig.BinaryPathName
t.Run("Name", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
srv, err := manager.GetService(serviceName)
must.NoError(t, err)
defer srv.Close()
must.Eq(t, serviceName, srv.Name(), must.Sprint("service name does not match"))
})
t.Run("Configure", func(t *testing.T) {
ci.Parallel(t)
t.Run("valid configuration", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
srv, err := manager.GetService(serviceName)
must.NoError(t, err, must.Sprint("service should be available"))
err = srv.Configure(WindowsServiceConfiguration{
StartType: StartDisabled,
DisplayName: "testing display name",
BinaryPathName: `c:\stub -with -arguments`,
})
must.NoError(t, err, must.Sprint("valid configuration should not error"))
directSrv, err := m.OpenService(serviceName)
must.NoError(t, err, must.Sprint("direct service should be available"))
defer directSrv.Close()
config, err := directSrv.Config()
must.NoError(t, err, must.Sprint("direct service config should be available"))
must.Eq(t, "testing display name", config.DisplayName, must.Sprint("display name does not match"))
must.Eq(t, `c:\stub -with -arguments`, config.BinaryPathName, must.Sprint("binary path name does not match"))
})
t.Run("invalid configuration", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
srv, err := manager.GetService(serviceName)
must.NoError(t, err, must.Sprint("service should be available"))
err = srv.Configure(WindowsServiceConfiguration{
DisplayName: "testing display name",
BinaryPathName: `c:\stub -with -arguments`,
})
must.ErrorContains(t, err, "parameter is incorrect", must.Sprint("invalid configuration should error"))
})
})
t.Run("Start", func(t *testing.T) {
ci.Parallel(t)
t.Run("when stopped", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
runnableSvc := runnableService(t, manager, binPath)
directSrv, err := m.OpenService(runnableSvc.Name())
must.NoError(t, err, must.Sprint("direct service should be available"))
defer directSrv.Close()
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
if status.State != svc.Stopped {
_, err := directSrv.Control(svc.Stop)
must.NoError(t, err, must.Sprint("direct stop should not fail"))
err = waitFor(context.Background(), func() (bool, error) {
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service should be queryable"))
return status.State == svc.Stopped, nil
})
must.NoError(t, err, must.Sprint("service must be stopped"))
}
must.NoError(t, runnableSvc.Start(), must.Sprint("service should start without error"))
status, err = directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
must.Eq(t, status.State, svc.Running, must.Sprint("service should be running"))
})
t.Run("when running", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
runnableSvc := runnableService(t, manager, binPath)
directSrv, err := m.OpenService(runnableSvc.Name())
must.NoError(t, err, must.Sprint("direct service should be available"))
defer directSrv.Close()
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
if status.State != svc.Running {
must.NoError(t, directSrv.Start(), must.Sprint("direct start should not fail"))
err := waitFor(context.Background(), func() (bool, error) {
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service should be queryable"))
return status.State == svc.Running, nil
})
must.NoError(t, err, must.Sprint("service must be running"))
}
must.NoError(t, runnableSvc.Start(), must.Sprint("service should start without error"))
status, err = directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
must.Eq(t, status.State, svc.Running, must.Sprint("service should be running"))
})
})
t.Run("Stop", func(t *testing.T) {
ci.Parallel(t)
t.Run("when stopped", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
runnableSvc := runnableService(t, manager, binPath)
directSrv, err := m.OpenService(runnableSvc.Name())
must.NoError(t, err, must.Sprint("direct service should be available"))
defer directSrv.Close()
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
if status.State != svc.Stopped {
_, err := directSrv.Control(svc.Stop)
must.NoError(t, err, must.Sprint("direct stop should not fail"))
err = waitFor(context.Background(), func() (bool, error) {
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service should be queryable"))
return status.State == svc.Stopped, nil
})
must.NoError(t, err, must.Sprint("service must be stopped"))
}
must.NoError(t, runnableSvc.Stop(), must.Sprint("service should stop without error"))
status, err = directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
must.Eq(t, status.State, svc.Stopped, must.Sprint("service should be stopped"))
})
t.Run("when running", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
runnableSvc := runnableService(t, manager, binPath)
directSrv, err := m.OpenService(runnableSvc.Name())
must.NoError(t, err, must.Sprint("direct service should be available"))
defer directSrv.Close()
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
if status.State != svc.Running {
must.NoError(t, directSrv.Start(), must.Sprint("direct start should not fail"))
err := waitFor(context.Background(), func() (bool, error) {
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service should be queryable"))
return status.State == svc.Running, nil
})
must.NoError(t, err, must.Sprint("service must be running"))
}
must.NoError(t, runnableSvc.Stop(), must.Sprint("service should stop without error"))
status, err = directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
must.Eq(t, status.State, svc.Stopped, must.Sprint("service should be stopped"))
})
})
t.Run("Delete", func(t *testing.T) {
ci.Parallel(t)
t.Run("when service exists", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
srv, err := manager.GetService(serviceName)
must.NoError(t, err, must.Sprint("service should be avaialble"))
defer srv.Close()
must.NoError(t, srv.Delete(), must.Sprint("service should be deleted"))
})
t.Run("when service does not exist", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
srv, err := manager.GetService(serviceName)
must.NoError(t, err, must.Sprint("service should be avaialble"))
defer srv.Close()
// Delete the service directly
directSrv, err := m.OpenService(serviceName)
must.NoError(t, err, must.Sprint("direct service should be available"))
defer directSrv.Close()
must.NoError(t, directSrv.Delete(), must.Sprint("service should be deleted"))
must.ErrorContains(t, srv.Delete(), "marked for deletion",
must.Sprint("service should have already been deleted"))
})
})
t.Run("IsRunning", func(t *testing.T) {
ci.Parallel(t)
t.Run("when service is not running", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
runnableSvc := runnableService(t, manager, binPath)
directSrv, err := m.OpenService(runnableSvc.Name())
must.NoError(t, err, must.Sprint("direct service should be available"))
defer directSrv.Close()
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
if status.State != svc.Stopped {
_, err := directSrv.Control(svc.Stop)
must.NoError(t, err, must.Sprint("direct stop should not fail"))
err = waitFor(context.Background(), func() (bool, error) {
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service should be queryable"))
return status.State == svc.Stopped, nil
})
must.NoError(t, err, must.Sprint("service must be stopped"))
}
srv, err := manager.GetService(directSrv.Name)
must.NoError(t, err, must.Sprint("service should be available"))
defer srv.Close()
result, err := srv.IsRunning()
must.NoError(t, err, must.Sprint("running check should not error"))
must.False(t, result, must.Sprint("should not show service as running"))
})
t.Run("when service is running", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
runnableSvc := runnableService(t, manager, binPath)
directSrv, err := m.OpenService(runnableSvc.Name())
must.NoError(t, err, must.Sprint("direct service should be available"))
defer directSrv.Close()
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
if status.State != svc.Running {
must.NoError(t, directSrv.Start(), must.Sprint("direct start should not fail"))
err := waitFor(context.Background(), func() (bool, error) {
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service should be queryable"))
return status.State == svc.Running, nil
})
must.NoError(t, err, must.Sprint("service must be running"))
}
srv, err := manager.GetService(directSrv.Name)
must.NoError(t, err, must.Sprint("service should be available"))
defer srv.Close()
result, err := srv.IsRunning()
must.NoError(t, err, must.Sprint("running check should not error"))
must.True(t, result, must.Sprint("should show service as running"))
})
})
t.Run("IsStopped", func(t *testing.T) {
ci.Parallel(t)
t.Run("when service is not running", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
runnableSvc := runnableService(t, manager, binPath)
directSrv, err := m.OpenService(runnableSvc.Name())
must.NoError(t, err, must.Sprint("direct service should be available"))
defer directSrv.Close()
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
if status.State != svc.Stopped {
_, err := directSrv.Control(svc.Stop)
must.NoError(t, err, must.Sprint("direct stop should not fail"))
err = waitFor(context.Background(), func() (bool, error) {
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service should be queryable"))
return status.State == svc.Stopped, nil
})
must.NoError(t, err, must.Sprint("service must be stopped"))
}
srv, err := manager.GetService(directSrv.Name)
must.NoError(t, err, must.Sprint("service should be available"))
defer srv.Close()
result, err := srv.IsStopped()
must.NoError(t, err, must.Sprint("running check should not error"))
must.True(t, result, must.Sprint("should show service as stopped"))
})
t.Run("when service is running", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
runnableSvc := runnableService(t, manager, binPath)
directSrv, err := m.OpenService(runnableSvc.Name())
must.NoError(t, err, must.Sprint("direct service should be available"))
defer directSrv.Close()
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service status should be available"))
if status.State != svc.Running {
must.NoError(t, directSrv.Start(), must.Sprint("direct start should not fail"))
err := waitFor(context.Background(), func() (bool, error) {
status, err := directSrv.Query()
must.NoError(t, err, must.Sprint("direct service should be queryable"))
return status.State == svc.Running, nil
})
must.NoError(t, err, must.Sprint("service must be running"))
}
srv, err := manager.GetService(directSrv.Name)
must.NoError(t, err, must.Sprint("service should be available"))
defer srv.Close()
result, err := srv.IsStopped()
must.NoError(t, err, must.Sprint("running check should not error"))
must.False(t, result, must.Sprint("should not show service as stopped"))
})
})
t.Run("EnableEventLog", func(t *testing.T) {
ci.Parallel(t)
t.Run("when service is not registered", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
srv, err := manager.GetService(serviceName)
must.NoError(t, err, must.Sprint("service should be available"))
defer srv.Close()
must.NoError(t, srv.EnableEventlog(), must.Sprint("could not enable eventlog"))
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
EVENTLOG_REGISTRY_PATH+`\`+serviceName,
registry.READ,
)
must.NoError(t, err, must.Sprint("registry key should be available"))
defer key.Close()
val, _, err := key.GetIntegerValue(EVENTLOG_SUPPORTED_EVENTS_KEY)
must.NoError(t, err, must.Sprint("registry key value should be available"))
must.Eq(t, EVENTLOG_SUPPORTED_EVENTS, uint32(val), must.Sprint("registry value should match"))
})
t.Run("when service is already registered", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
srv, err := manager.GetService(serviceName)
must.NoError(t, err, must.Sprint("service should be available"))
defer srv.Close()
must.NoError(t, srv.EnableEventlog(), must.Sprint("could not enable eventlog"))
// Modify value in registry
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
EVENTLOG_REGISTRY_PATH+`\`+serviceName,
registry.ALL_ACCESS,
)
err = key.SetDWordValue(EVENTLOG_SUPPORTED_EVENTS_KEY, 1)
must.NoError(t, err, must.Sprint("could not modify registry value"))
// Now enable and verify value is correct
must.NoError(t, srv.EnableEventlog(), must.Sprint("failed to enable eventlog"))
val, _, err := key.GetIntegerValue(EVENTLOG_SUPPORTED_EVENTS_KEY)
must.NoError(t, err, must.Sprint("registry value should be available"))
must.Eq(t, EVENTLOG_SUPPORTED_EVENTS, uint32(val), must.Sprint("registry value should match"))
})
})
t.Run("DisableEventLog", func(t *testing.T) {
ci.Parallel(t)
t.Run("when service is not registered", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
srv, err := manager.GetService(serviceName)
must.NoError(t, err, must.Sprint("service should be available"))
defer srv.Close()
must.NoError(t, srv.DisableEventlog(), must.Sprint("eventlog disable should not error"))
})
t.Run("when service is registered", func(t *testing.T) {
ci.Parallel(t)
m, manager := makeManagers(t)
serviceName := generateStubService(t, m)
srv, err := manager.GetService(serviceName)
must.NoError(t, err, must.Sprint("service should be available"))
defer srv.Close()
must.NoError(t, srv.EnableEventlog(), must.Sprint("eventlog enable should not error"))
must.NoError(t, srv.DisableEventlog(), must.Sprint("eventlog disable should not error"))
_, err = registry.OpenKey(registry.LOCAL_MACHINE,
EVENTLOG_REGISTRY_PATH+`\`+serviceName,
registry.READ,
)
must.ErrorIs(t, err, fs.ErrNotExist, must.Sprint("registry key should no longer exist"))
})
})
}
func generateServiceName() string {
id, err := uuid.GenerateUUID()
if err != nil {
panic(err)
}
return id[:5]
}
func generateStubService(t *testing.T, m *mgr.Mgr) string {
t.Helper()
id := generateServiceName()
_, err := m.CreateService(id, `c:\stub`, mgr.Config{})
must.NoError(t, err, must.Sprint("failed to generate stub service"))
t.Cleanup(func() { deleteStubService(t, m, id) })
return id
}
func deleteStubService(t *testing.T, m *mgr.Mgr, svcId string) {
t.Helper()
srvc, err := m.OpenService(svcId)
if err != nil {
// If the service doesn't exist, then deletion is done so not
// an error. Otherwise, force an error.
must.ErrorContains(t, err, "service does not exist", must.Sprint("failed to open service"))
return
}
status, err := srvc.Query()
must.NoError(t, err, must.Sprint("failed to query service"))
if status.State != svc.Stopped {
status, err = srvc.Control(svc.Stop)
must.NoError(t, err, must.Sprint("failed to stop service"))
err := waitFor(context.Background(), func() (bool, error) {
status, err := srvc.Query()
must.NoError(t, err, must.Sprint("failed to query service"))
return status.State == svc.Stopped, nil
})
must.NoError(t, err, must.Sprintf("could not stop service for deletion - %s", svcId))
}
if err := srvc.Delete(); err != nil {
must.ErrorContains(t, err, "service has been marked for deletion", must.Sprint("failed to delete service"))
}
}
func makeManagers(t *testing.T) (*mgr.Mgr, WindowsServiceManager) {
t.Helper()
winM, err := NewWindowsServiceManager()
must.NoError(t, err, must.Sprint("failed to create service manager"))
m, err := mgr.Connect()
must.NoError(t, err, must.Sprint("failed to connect to windows service manager"))
t.Cleanup(func() {
winM.Close()
m.Disconnect()
})
return m, winM
}
func runnableService(t *testing.T, m WindowsServiceManager, binPath string) WindowsService {
t.Helper()
runnableSvc, err := m.CreateService(generateServiceName(), binPath,
WindowsServiceConfiguration{StartType: StartManual, BinaryPathName: binPath})
must.NoError(t, err, must.Sprint("failed to create runnable service"))
t.Cleanup(func() { runnableSvc.Close() })
return runnableSvc
}

View File

@@ -129,6 +129,10 @@ You may, however, may pass the following configuration options as CLI arguments:
- `-encrypt`: Set the Serf encryption key. See the [Encryption Overview][] for
more details.
- `-eventlog`: Equivalent to the [eventlog.enabled][] config option.
- `-eventlog-level`: Equivalent to the [eventlog.level][] config option.
- `-join=<address>`: Address of another agent to join upon starting up. This can
be specified multiple times to specify multiple agents to join.
@@ -231,6 +235,8 @@ You may, however, may pass the following configuration options as CLI arguments:
[datacenter]: /nomad/docs/configuration#datacenter
[enabled]: /nomad/docs/configuration/acl#enabled
[encryption overview]: /nomad/docs/secure/traffic/gossip-encryption
[eventlog.enabled]: /nomad/docs/configuration#eventlog_enabled
[eventlog.level]: /nomad/docs/configuration#eventlog_level
[key_file]: /nomad/docs/configuration/consul#key_file
[log_include_location]: /nomad/docs/configuration#log_include_location
[log_json]: /nomad/docs/configuration#log_json

View File

@@ -0,0 +1,22 @@
---
layout: docs
page_title: 'nomad windows command reference'
description: |
The `nomad windows` command interacts with the Windows host platform. Install or uninstall Nomad as a Windows service.
---
# `nomad windows` command reference
Use the `windows` command to interact with Windows host platforms.
## Usage
Usage: `nomad windows <subcommand> [options]`
Run `nomad windows <subcommand> -h` for help on that subcommand. The following subcommands are available:
- [`windows service install`][install] - Install Nomad as a Windows service
- [`windows service uninstall`][uninstall] - Uninstall Nomad as a Windows service
[install]: /nomad/commands/windows/service-install
[uninstall]: /nomad/commands/windows/service-uninstall

View File

@@ -0,0 +1,50 @@
---
layout: docs
page_title: 'nomad windows service install command reference'
description: |
The `nomad windows service install` command installs the Nomad binary
and creates a Windows service.
---
# `nomad windows service install` command reference
The `windows service install` command installs the Nomad binary and
creates a Windows service.
## Usage
```plaintext
nomad windows service install
```
The `windows service install` command installs the `nomad` binary used to
run this command, creates a data and configuration directory, writes a basic
Nomad configuration file, creates a Windows service to run Nomad, and
registers the service with Windows Event Log.
If Nomad has been previously installed using this command, subsequent
executions will do the following:
1. Stop the service if it is running
1. Install the currently executing nomad binary
1. Ensure data and configuration directories exist
1. Write a configuration file if no configuration files are found
1. Update the service if needed
1. Update the Event Log configuration if needed.
## Options
- `-config-dir <dir>`: Directory to hold the Nomad agent configuration.
Defaults to "{{.ProgramFiles}}\HashiCorp\nomad\bin"
- `-data-dir <dir>`: Directory to hold the Nomad agent state. Defaults
to "{{.ProgramData}}\HashiCorp\nomad\data"
- `-install-dir <dir>`: Directory to install the Nomad binary. Defaults
to "{{.ProgramData}}\HashiCorp\nomad\config"
- `-reinstall`: Allow the nomad Windows service to be stopped during install.
## General options
@include 'general_options.mdx'

View File

@@ -0,0 +1,22 @@
---
layout: docs
page_title: 'nomad windows service uninstall command reference'
description: |
The `nomad windows service uninstall` command removes the Nomad
Windows service.
---
# `nomad windows service uninstall` command reference
The `windows service uninstall` command removes the Nomad Windows service.
## Usage
```plaintext
nomad windows service uninstall
```
The `windows service uninstall` command stops the Nomad service if
it is currently running, deregisters the service with the Windows Event Log,
and removes the Windows service. This command does not remove the installed
Nomad binary or the data and configuration directories.

View File

@@ -176,6 +176,16 @@ testing.
This option only works on Unix based systems. The log level inherits from
the Nomad agent log set in `log_level`
- `eventlog` - This is a nested object that configures the behavior with
with Windows Event Log. The following parameters are available:
- `enabled` - Enable sending Nomad agent logs to the Windows Event Log.
- `level` - `(string: "ERROR")` - Specifies the verbosity of logs the Nomad
agent outputs. Valid log levels include `ERROR`, `WARN`, or `INFO` in
increasing order of verbosity. Level must be of equal or less verbosity as
defined for the [`log_level`](#log_level) parameter.
- `http_api_response_headers` `(map<string|string>: nil)` - Specifies
user-defined headers to add to the HTTP API responses.

View File

@@ -7,9 +7,12 @@ description: |-
# Install Nomad as a Windows service
Nomad can be run as a native Windows service. In order to do this, you will need
to register the Nomad application with the Windows Service Control Manager using
[`sc.exe`], configure Nomad to log to a file, and then start the Nomad service.
You may run Nomad as a native Windows service. Use the [windows service install][]
command to install Nomad and create the Windows service.
You may also set up the Nomad Windows service manually. Use [`sc.exe`] to register
the Nomad application with the Windows Service Control Manager, configure Nomad to
log to a file, and then start the Nomad service.
~> **Note:** These steps should be run in a PowerShell session with Administrator
capabilities.
@@ -23,7 +26,7 @@ argument should include the fully qualified path to the Nomad executable and any
arguments to the nomad command: agent, -config, etc.
```plaintext
sc.exe create "Nomad" binPath="«full path to nomad.exe» agent -config=«path to config file or directory»" start= auto
sc.exe create "Nomad" binPath="«full path to nomad.exe» agent -config=«path to config file or directory»" start=auto
[SC] CreateService SUCCESS
```
@@ -93,3 +96,4 @@ restart of Nomad service is not sufficient.
[`sc.exe`]: https://msdn.microsoft.com/en-us/library/windows/desktop/ms682107(v=vs.85).aspx
[download]: /nomad/downloads
[logging]: /nomad/docs/configuration#log_file
[windows service install]: /nomad/docs/commands/windows/service-install

View File

@@ -1053,5 +1053,22 @@
"path": "volume/status"
}
]
},
{
"title": "windows",
"routes": [
{
"title": "Overview",
"path": "windows"
},
{
"title": "service install",
"path": "windows/service-install"
},
{
"title": "service uninstall",
"path": "windows/service-uninstall"
}
]
}
]