Skip to content

Commit

Permalink
feat: add Windows service support (#429)
Browse files Browse the repository at this point in the history
  • Loading branch information
jackwotherspoon committed Sep 11, 2023
1 parent d97e7c1 commit 2ad57d6
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

# Compiled binary
/alloydb-auth-proxy
/alloydb-auth-proxy.exe

/key.json

/logs/


### SAMPLES
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
golang.org/x/oauth2 v0.11.0
golang.org/x/sys v0.11.0
google.golang.org/api v0.138.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1804,6 +1804,8 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
Expand Down
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !windows
// +build !windows

package main

import (
Expand Down
127 changes: 127 additions & 0 deletions main_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"context"
"errors"
"os"
"path/filepath"
"time"

"github.com/GoogleCloudPlatform/alloydb-auth-proxy/cmd"
"github.com/GoogleCloudPlatform/alloydb-auth-proxy/internal/log"
"golang.org/x/sys/windows/svc"
"gopkg.in/natefinch/lumberjack.v2"
)

type windowsService struct{}

func (m *windowsService) Execute(_ []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
// start the service
changes <- svc.Status{State: svc.StartPending}

// set up the log file
exePath, err := os.Executable()
if err != nil {
changes <- svc.Status{State: svc.StopPending}
return true, 101 // service specific exit code=101
}

logFolder := filepath.Join(filepath.Dir(exePath), "logs")
os.Mkdir(logFolder, 0644) // ignore all errors

logFile := &lumberjack.Logger{
Filename: filepath.Join(logFolder, "alloydb-auth-proxy.log"),
MaxSize: 50, // megabytes
MaxBackups: 10,
MaxAge: 30, //days
}

logger := log.NewStdLogger(logFile, logFile)
logger.Infof("Starting...")

// start the main command
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

app := cmd.NewCommand(cmd.WithLogger(logger))

cmdErrCh := make(chan error, 1)
go func() {
cmdErrCh <- app.ExecuteContext(ctx)
}()

// now running
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}

var cmdErr error

loop:
for {
select {
case err := <-cmdErrCh:
cmdErr = err
break loop

case c := <-r:
switch c.Cmd {
case svc.Interrogate:
changes <- c.CurrentStatus
// testing deadlock from https://code.google.com/archive/p/winsvc/issues/4
time.Sleep(100 * time.Millisecond)
changes <- c.CurrentStatus

case svc.Stop, svc.Shutdown:
cancel()

default:
logger.Errorf("unexpected control request #%d", c)
}
}
}

// start shutting down
logger.Infof("Stopping...")

changes <- svc.Status{State: svc.StopPending}

if cmdErr != nil && errors.Is(cmdErr, context.Canceled) {
logger.Errorf("Unexpected error: %v", cmdErr)
return true, 2
}

return false, 0
}

func main() {
// determine if running as a windows service
inService, err := svc.IsWindowsService()
if err != nil {
os.Exit(99) // failed to determine service status
}

// running as service?
if inService {
err := svc.Run("alloydb-auth-proxy", &windowsService{})
if err != nil {
os.Exit(100) // failed to execute service
}
return
}

// run as commandline
cmd.Execute()
}
88 changes: 88 additions & 0 deletions windows-service-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# AlloyDB Auth Proxy Windows Service Guide

This document covers running the *AlloyDB Auth Proxy* as a service
on the Windows operating system.

It was originally built and tested using Go 1.20.2 on Windows Server 2019.

## Install the Windows Service

Prerequisites: A built binary for Windows of the AlloyDB Auth Proxy is required.
Either build it from source or
[download a release](https://github.com/GoogleCloudPlatform/alloydb-auth-proxy/releases)
of a Windows pre-built version, e.g. `alloydb-auth-proxy.x64.exe`.

First, install the binary by:

1. Create a new empty folder, e.g. `C:\Program Files\alloydb-auth-proxy`
2. Copy the binary and helper batch files
3. Modify the batch files as needed:
- `SERVICE` is the Windows internal service name (as shown in the Task Manager)
- `DISPLAYNAME` is the service name (as shown in the Windows Administration Console (MMC))
- `CREDENTIALSFILE` is the *full* path to the credentials file, where `%~dp0` points to the full path of the script file folder.
- `INSTANCEURI` is the AlloyDB Instance URI in the format of `projects/<PROJECT>/locations/<REGION>/clusters/<CLUSTER>/instances/<INSTANCE>`
- Please note that the `--credentials-file \"%CREDENTIALSFILE%\"` argument is optional and is not needed if the local machine runs within the Google Cloud Compute Engine and "defaults" to the VM instance service account.
4. Grant *read & execute* access to the `Network Service` user
5. Create a `logs` sub-folder, e.g. `C:\Program Files\alloydb-auth-proxy\logs`
6. Grant *modify* access to the `Network Service` user
7. Run the `windows_install_service.bat` batch file within an *elevated* command line prompt (read: *Run as Administrator*).

After that, perform the setup:

1. Copy the JSON credentials file, if required
2. Modify the `windows_install_service.bat` file to your needs
3. Run the `windows_install_service.bat` file from the commandline

Please see the FAQ below for common error messages.

## Uninstall the Windows Service

To uninstall the Windows Service, perform the following steps:

1. Modify the `windows_remove_service.bat` file to your needs
2. Run the `windows_remove_service.bat` file from the commandline

## FAQ

### Error Message: *Access is denied*

The error message `Access is denied.` (or `System error 5 has occurred.`) occurs when
trying to start the installed service but the service account does not have access
to the service's file directory.

Usually this is the *Network Service* built-in user.

Please note that write access is also required for creating and managing the log files, e.g.:

- `alloydb-auth-proxy.log`
- `alloydb-auth-proxy-2016-11-04T18-30-00.000.log`

### Error Message: *The specified service has been marked for deletion.*

The error message `The specified service has been marked for deletion.` occurs when
reinstalling the service and the previous deletion request could not be completed
(e.g. because the service was still running or opened in the service manager).

In this case, the local machine needs to be restarted.

### Why not running as the *System* user?

Since the AlloyDB Auth Proxy does not require any file system access,
besides the log files, extensive operating system access is not required.

The *Network Service* accounts allow binding ports while not granting
access to file system resources.

### Why not using *Automatic (Delayed Start)* startup type?

The service is installed in the *Automatic* startup type, by default.

The alternative *Automatic (Delayed Start)* startup type was introduced
by Microsoft for services that are not required for operating system operations
like Windows Update and similar services.

However, if the primary purpose of the local machine is to provide services
which require access to the cloud database, then the start of the service
should not be delayed.

Delayed services might be started even minutes after operating system startup.
14 changes: 14 additions & 0 deletions windows_install_service.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@echo off

setlocal

set SERVICE=alloydb-auth-proxy
set DISPLAYNAME=Google AlloyDB Auth Proxy
set CREDENTIALSFILE=%~dp0key.json
set INSTANCEURI=projects/<PROJECT>/locations/<REGION>/clusters/<CLUSTER>/instances/<INSTANCE>

sc.exe create "%SERVICE%" binPath= "\"%~dp0alloydb-auth-proxy.exe\" --credentials-file \"%CREDENTIALSFILE%\" %INSTANCEURI%" obj= "NT AUTHORITY\Network Service" start= auto displayName= "%DISPLAYNAME%"
sc.exe failure "%SERVICE%" reset= 0 actions= restart/0/restart/0/restart/0
net start "%SERVICE%"

endlocal
10 changes: 10 additions & 0 deletions windows_remove_service.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@echo off

setlocal

set SERVICE=alloydb-auth-proxy

net stop "%SERVICE%"
sc.exe delete "%SERVICE%"

endlocal

0 comments on commit 2ad57d6

Please sign in to comment.