diff --git a/.gitignore b/.gitignore index c90dbfe6..81a681be 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,10 @@ # Compiled binary /alloydb-auth-proxy +/alloydb-auth-proxy.exe /key.json - +/logs/ ### SAMPLES diff --git a/go.mod b/go.mod index 04309d7e..25c017f3 100644 --- a/go.mod +++ b/go.mod @@ -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 ( diff --git a/go.sum b/go.sum index 9bb0f91d..d0912242 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 485e2f62..a0e01026 100644 --- a/main.go +++ b/main.go @@ -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 ( diff --git a/main_windows.go b/main_windows.go new file mode 100644 index 00000000..90e77e0d --- /dev/null +++ b/main_windows.go @@ -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() +} diff --git a/windows-service-guide.md b/windows-service-guide.md new file mode 100644 index 00000000..6fa95cd6 --- /dev/null +++ b/windows-service-guide.md @@ -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//locations//clusters//instances/` + - 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. diff --git a/windows_install_service.bat b/windows_install_service.bat new file mode 100644 index 00000000..6a2e9bb5 --- /dev/null +++ b/windows_install_service.bat @@ -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//locations//clusters//instances/ + +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 diff --git a/windows_remove_service.bat b/windows_remove_service.bat new file mode 100644 index 00000000..cb57f981 --- /dev/null +++ b/windows_remove_service.bat @@ -0,0 +1,10 @@ +@echo off + +setlocal + +set SERVICE=alloydb-auth-proxy + +net stop "%SERVICE%" +sc.exe delete "%SERVICE%" + +endlocal