Skip to content

Commit

Permalink
Support running SPIRE as a Windows service (#3625)
Browse files Browse the repository at this point in the history
* Support running SPIRE as a Windows service

Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
  • Loading branch information
amartinezfayo committed Jan 4, 2023
1 parent 4e8aee5 commit a7df8a5
Show file tree
Hide file tree
Showing 13 changed files with 578 additions and 14 deletions.
5 changes: 3 additions & 2 deletions cmd/spire-agent/cli/cli.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"context"
stdlog "log"

"github.com/mitchellh/cli"
Expand All @@ -17,7 +18,7 @@ type CLI struct {
AllowUnknownConfig bool
}

func (cc *CLI) Run(args []string) int {
func (cc *CLI) Run(ctx context.Context, args []string) int {
c := cli.NewCLI("spire-agent", version.Version())
c.Args = args
c.Commands = map[string]cli.CommandFactory{
Expand All @@ -37,7 +38,7 @@ func (cc *CLI) Run(args []string) int {
return &api.WatchCLI{}, nil
},
"run": func() (cli.Command, error) {
return run.NewRunCommand(cc.LogOptions, cc.AllowUnknownConfig), nil
return run.NewRunCommand(ctx, cc.LogOptions, cc.AllowUnknownConfig), nil
},
"healthcheck": func() (cli.Command, error) {
return healthcheck.NewHealthCheckCommand(), nil
Expand Down
14 changes: 10 additions & 4 deletions cmd/spire-agent/cli/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,19 @@ type experimentalConfig struct {
}

type Command struct {
ctx context.Context
logOptions []log.Option
env *common_cli.Env
allowUnknownConfig bool
}

func NewRunCommand(logOptions []log.Option, allowUnknownConfig bool) cli.Command {
return newRunCommand(common_cli.DefaultEnv, logOptions, allowUnknownConfig)
func NewRunCommand(ctx context.Context, logOptions []log.Option, allowUnknownConfig bool) cli.Command {
return newRunCommand(ctx, common_cli.DefaultEnv, logOptions, allowUnknownConfig)
}

func newRunCommand(env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command {
func newRunCommand(ctx context.Context, env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command {
return &Command{
ctx: ctx,
env: env,
logOptions: logOptions,
allowUnknownConfig: allowUnknownConfig,
Expand Down Expand Up @@ -183,7 +185,11 @@ func (cmd *Command) Run(args []string) int {

a := agent.New(c)

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
ctx := cmd.ctx
if ctx == nil {
ctx = context.Background()
}
ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer stop()

err = a.Run(ctx)
Expand Down
3 changes: 2 additions & 1 deletion cmd/spire-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"os"

"github.com/spiffe/spire/cmd/spire-agent/cli"
"github.com/spiffe/spire/pkg/common/entrypoint"
)

func main() {
os.Exit(new(cli.CLI).Run(os.Args[1:]))
os.Exit(entrypoint.NewEntryPoint(new(cli.CLI).Run).Main())
}
5 changes: 3 additions & 2 deletions cmd/spire-server/cli/cli.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"context"
stdlog "log"

"github.com/mitchellh/cli"
Expand All @@ -25,7 +26,7 @@ type CLI struct {
}

// Run configures the server CLI commands and subcommands.
func (cc *CLI) Run(args []string) int {
func (cc *CLI) Run(ctx context.Context, args []string) int {
c := cli.NewCLI("spire-server", version.Version())
c.Args = args
c.Commands = map[string]cli.CommandFactory{
Expand Down Expand Up @@ -93,7 +94,7 @@ func (cc *CLI) Run(args []string) int {
return federation.NewUpdateCommand(), nil
},
"run": func() (cli.Command, error) {
return run.NewRunCommand(cc.LogOptions, cc.AllowUnknownConfig), nil
return run.NewRunCommand(ctx, cc.LogOptions, cc.AllowUnknownConfig), nil
},
"token generate": func() (cli.Command, error) {
return token.NewGenerateCommand(), nil
Expand Down
14 changes: 10 additions & 4 deletions cmd/spire-server/cli/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,13 @@ type rateLimitConfig struct {
UnusedKeys []string `hcl:",unusedKeys"`
}

func NewRunCommand(logOptions []log.Option, allowUnknownConfig bool) cli.Command {
return newRunCommand(common_cli.DefaultEnv, logOptions, allowUnknownConfig)
func NewRunCommand(ctx context.Context, logOptions []log.Option, allowUnknownConfig bool) cli.Command {
return newRunCommand(ctx, common_cli.DefaultEnv, logOptions, allowUnknownConfig)
}

func newRunCommand(env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command {
func newRunCommand(ctx context.Context, env *common_cli.Env, logOptions []log.Option, allowUnknownConfig bool) *Command {
return &Command{
ctx: ctx,
env: env,
logOptions: logOptions,
allowUnknownConfig: allowUnknownConfig,
Expand All @@ -182,6 +183,7 @@ func newRunCommand(env *common_cli.Env, logOptions []log.Option, allowUnknownCon

// Run Command struct
type Command struct {
ctx context.Context
logOptions []log.Option
env *common_cli.Env
allowUnknownConfig bool
Expand Down Expand Up @@ -241,7 +243,11 @@ func (cmd *Command) Run(args []string) int {

s := server.New(*c)

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
ctx := cmd.ctx
if ctx == nil {
ctx = context.Background()
}
ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer stop()

err = s.Run(ctx)
Expand Down
3 changes: 2 additions & 1 deletion cmd/spire-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"os"

"github.com/spiffe/spire/cmd/spire-server/cli"
"github.com/spiffe/spire/pkg/common/entrypoint"
)

func main() {
os.Exit(new(cli.CLI).Run(os.Args[1:]))
os.Exit(entrypoint.NewEntryPoint(new(cli.CLI).Run).Main())
}
19 changes: 19 additions & 0 deletions doc/spire_agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,25 @@ the following flags are available:
| `-trustBundleUrl` | URL to download the SPIRE server CA bundle | |
| `-trustDomain` | The trust domain that this agent belongs to (should be no more than 255 characters) | |

#### Running SPIRE Agent as a Windows service

On Windows platform, SPIRE Agent can optionally be run as a Windows service. When running as a Windows service, the only command supported is the `run` command.

_Note: SPIRE does not automatically create the service in the system, it must be created by the user.
When starting the service, all the arguments to execute SPIRE Agent with the `run` command must be passed as service arguments._

##### Example to create the SPIRE Agent Windows service

```bash
> sc.exe create spire-agent binpath=c:\spire\bin\spire-agent.exe
```

##### Example to run the SPIRE Agent Windows service

```bash
> sc.exe start spire-agent run -config c:\spire\conf\agent\agent.conf
```

### `spire-agent api fetch`

Calls the workload API to fetch an X509-SVID. This command is aliased to `spire-agent api fetch x509`.
Expand Down
19 changes: 19 additions & 0 deletions doc/spire_server.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,25 @@ Most of the configuration file above options have identical command-line counter
| `-socketPath` | Path to bind the SPIRE Server API socket to | |
| `-trustDomain` | The trust domain that this server belongs to (should be no more than 255 characters) | |

#### Running SPIRE Server as a Windows service

On Windows platform, SPIRE Server can optionally be run as a Windows service. When running as a Windows service, the only command supported is the `run` command.

_Note: SPIRE does not automatically create the service in the system, it must be created by the user.
When starting the service, all the arguments to execute SPIRE Server with the `run` command must be passed as service arguments._

##### Example to create the SPIRE Server Windows service

```bash
> sc.exe create spire-server binpath=c:\spire\bin\spire-server.exe
```

##### Example to run the SPIRE Server Windows service

```bash
> sc.exe start spire-server run -config c:\spire\conf\server\server.conf
```

### `spire-server token generate`

Generates one node join token and creates a registration entry for it. This token can be used to
Expand Down
23 changes: 23 additions & 0 deletions pkg/common/entrypoint/entrypoint_posix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//go:build !windows
// +build !windows

package entrypoint

import (
"context"
"os"
)

type EntryPoint struct {
runCmdFn func(ctx context.Context, args []string) int
}

func NewEntryPoint(runFn func(ctx context.Context, args []string) int) *EntryPoint {
return &EntryPoint{
runCmdFn: runFn,
}
}

func (e *EntryPoint) Main() int {
return e.runCmdFn(context.Background(), os.Args[1:])
}
21 changes: 21 additions & 0 deletions pkg/common/entrypoint/entrypoint_posix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//go:build !windows
// +build !windows

package entrypoint

import (
"context"
"testing"

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

func TestEntryPoint(t *testing.T) {
assert.Equal(t,
NewEntryPoint(func(ctx context.Context, args []string) int { return 0 }).Main(),
0)

assert.Equal(t,
NewEntryPoint(func(ctx context.Context, args []string) int { return 1 }).Main(),
1)
}
73 changes: 73 additions & 0 deletions pkg/common/entrypoint/entrypoint_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//go:build windows
// +build windows

package entrypoint

import (
"context"
"fmt"
"os"

"golang.org/x/sys/windows/svc"
)

type systemCaller interface {
IsWindowsService() (bool, error)
Run(name string, handler svc.Handler) error
}

type systemCall struct {
}

func (s *systemCall) IsWindowsService() (bool, error) {
return svc.IsWindowsService()
}

func (s *systemCall) Run(name string, handler svc.Handler) error {
return svc.Run(name, handler)
}

type EntryPoint struct {
handler svc.Handler
runCmdFn func(ctx context.Context, args []string) int
sc systemCaller
}

func NewEntryPoint(runCmdFn func(ctx context.Context, args []string) int) *EntryPoint {
return &EntryPoint{
runCmdFn: runCmdFn,
handler: &service{
executeServiceFn: func(ctx context.Context, stop context.CancelFunc, args []string) int {
defer stop()
retCode := runCmdFn(ctx, args[1:])
return retCode
},
},
sc: &systemCall{},
}
}

func (e *EntryPoint) Main() int {
// Determining if SPIRE is running as a Windows service is done
// with a best-effort approach. If there is an error, just fallback
// to the behavior of not running as a Windows service.
isWindowsService, err := e.sc.IsWindowsService()
if err != nil {
fmt.Fprintf(os.Stderr, "Could not determine if running as a Windows service: %v", err)
}
if isWindowsService {
errChan := make(chan error)
go func() {
// Since the service runs in its own process, the service name is ignored.
// https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-startservicectrldispatcherw
errChan <- e.sc.Run("", e.handler)
}()
err = <-errChan
if err != nil {
return 1
}
return 0
}

return e.runCmdFn(context.Background(), os.Args[1:])
}

0 comments on commit a7df8a5

Please sign in to comment.