2023-06-11 20:40:47 -04:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
2024-05-22 11:11:48 -07:00
package cache
2023-06-11 20:40:47 -04:00
import (
"context"
2023-11-13 13:36:28 -08:00
stderrors "errors"
2023-06-11 20:40:47 -04:00
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
2023-10-30 11:24:23 -07:00
"time"
2023-06-11 20:40:47 -04:00
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/errors"
"github.com/mitchellh/cli"
"github.com/mitchellh/go-homedir"
"github.com/posener/complete"
2023-11-03 14:59:42 -07:00
"gopkg.in/natefinch/lumberjack.v2"
2023-06-11 20:40:47 -04:00
)
const (
dotDirname = ".boundary"
pidFileName = "cache.pid"
logFileName = "cache.log"
// Mark of process as having been started in the background
2024-05-22 11:11:48 -07:00
backgroundEnvName = "_BOUNDARY_CACHE_BACKGROUND"
2023-06-11 20:40:47 -04:00
backgroundEnvVal = "1"
)
var (
_ cli . Command = ( * StartCommand )( nil )
_ cli . CommandAutocomplete = ( * StartCommand )( nil )
)
type StartCommand struct {
* base . Command
2023-11-30 08:20:57 -08:00
flagRefreshInterval time . Duration
2023-12-20 11:07:38 -08:00
flagRecheckSupportInterval time . Duration
2023-11-30 08:20:57 -08:00
flagMaxSearchStaleness time . Duration
flagMaxSearchRefreshTimeout time . Duration
flagDatabaseUrl string
flagLogLevel string
flagLogFormat string
flagStoreDebug bool
flagBackground bool
2023-06-11 20:40:47 -04:00
}
func ( c * StartCommand ) Synopsis () string {
2024-05-22 11:11:48 -07:00
return "Start a Boundary cache"
2023-06-11 20:40:47 -04:00
}
func ( c * StartCommand ) Help () string {
helpText := `
2024-05-22 11:11:48 -07:00
Usage: boundary cache start [options]
2023-06-11 20:40:47 -04:00
2024-05-22 11:11:48 -07:00
Start a cache:
2023-06-11 20:40:47 -04:00
2024-05-22 11:11:48 -07:00
$ boundary cache start
2023-06-11 20:40:47 -04:00
For a full list of examples, please see the documentation.
` + c . Flags (). Help ()
return strings . TrimSpace ( helpText )
}
func ( c * StartCommand ) Flags () * base . FlagSets {
2024-01-26 15:56:41 -08:00
set := c . FlagSet ( base . FlagSetNone )
2023-06-11 20:40:47 -04:00
f := set . NewFlagSet ( "Command Options" )
f . StringVar ( & base . StringVar {
Name : "log-level" ,
Target : & c . flagLogLevel ,
EnvVar : "BOUNDARY_LOG_LEVEL" ,
Completion : complete . PredictSet ( "trace" , "debug" , "info" , "warn" , "err" ),
Usage : "Log verbosity level, mostly as a fallback for events. Supported values (in order of more detail to less) are " +
"\"trace\", \"debug\", \"info\", \"warn\", and \"err\"." ,
})
f . StringVar ( & base . StringVar {
Name : "log-format" ,
Target : & c . flagLogFormat ,
Completion : complete . PredictSet ( "standard" , "json" ),
Usage : `Log format, mostly as a fallback for events. Supported values are "standard" and "json".` ,
})
f . StringVar ( & base . StringVar {
Name : "database-url" ,
Target : & c . flagDatabaseUrl ,
Usage : `If set, specifies the URL used to connect to the sqlite database (store) for caching. This can refer to a file on disk (file://) from which a URL will be read; an env var (env://) from which the URL will be read; or a direct database URL.` ,
2024-01-26 15:56:41 -08:00
Hidden : true ,
2023-06-11 20:40:47 -04:00
})
2023-10-30 11:24:23 -07:00
f . DurationVar ( & base . DurationVar {
2023-11-30 08:20:57 -08:00
Name : "refresh-interval" ,
2023-10-30 11:24:23 -07:00
Target : & c . flagRefreshInterval ,
2023-12-20 11:07:38 -08:00
Usage : `Specifies the interval between refresh token supported cache refreshes.` ,
2023-11-30 08:20:57 -08:00
Default : daemon . DefaultRefreshInterval ,
2023-10-30 11:24:23 -07:00
})
f . DurationVar ( & base . DurationVar {
2023-12-20 11:07:38 -08:00
Name : "recheck-support-interval" ,
Target : & c . flagRecheckSupportInterval ,
Usage : `Specifies the interval between checking if a boundary instances is supported when it previously was not.` ,
Default : daemon . DefaultRecheckSupportInterval ,
Hidden : true ,
2023-11-30 08:20:57 -08:00
})
f . DurationVar ( & base . DurationVar {
Name : "max-search-staleness" ,
Target : & c . flagMaxSearchStaleness ,
2023-12-20 11:07:38 -08:00
Usage : `Specifies the duration of time that can pass since the resource was last updated before performing a search waits for the resources being refreshed first.` ,
2023-11-30 08:20:57 -08:00
Default : daemon . DefaultSearchStaleness ,
})
f . DurationVar ( & base . DurationVar {
Name : "max-search-refresh-timeout" ,
Target : & c . flagMaxSearchRefreshTimeout ,
2023-12-20 11:07:38 -08:00
Usage : `If a search request triggers a best effort refresh, this specifies how long the refresh should run before timing out.` ,
2023-11-30 08:20:57 -08:00
Default : daemon . DefaultSearchRefreshTimeout ,
2023-06-11 20:40:47 -04:00
})
f . BoolVar ( & base . BoolVar {
Name : "store-debug" ,
Target : & c . flagStoreDebug ,
Default : false ,
2024-02-22 10:20:48 -08:00
Usage : `Turn on sqlite query debugging. This is deprecated. Users should use -log-level=debug instead.` ,
2023-06-11 20:40:47 -04:00
Aliases : [] string { "d" },
2024-02-22 10:20:48 -08:00
Hidden : true ,
2023-06-11 20:40:47 -04:00
})
f . BoolVar ( & base . BoolVar {
Name : "background" ,
Target : & c . flagBackground ,
Default : false ,
2024-05-22 11:11:48 -07:00
Usage : `Run the cache daemon in the background` ,
2023-06-11 20:40:47 -04:00
})
return set
}
func ( c * StartCommand ) AutocompleteArgs () complete . Predictor {
return complete . PredictNothing
}
func ( c * StartCommand ) AutocompleteFlags () complete . Flags {
return c . Flags (). Completions ()
}
func ( c * StartCommand ) Run ( args [] string ) int {
ctx , cancel := context . WithCancel ( c . Context )
defer cancel ()
var err error
f := c . Flags ()
if err := f . Parse ( args ); err != nil {
c . PrintCliError ( err )
return base . CommandUserError
}
dotDir , err := DefaultDotDirectory ( ctx )
if err != nil {
return base . CommandCliError
}
2023-11-03 14:59:42 -07:00
if err := os . MkdirAll ( dotDir , 0 o700 ); err != nil {
c . PrintCliError ( err )
return base . CommandCliError
}
2023-06-11 20:40:47 -04:00
2023-11-30 08:20:57 -08:00
continueRun , writers , cleanup , err := c . makeBackground ( ctx , dotDir )
2023-06-11 20:40:47 -04:00
defer func () {
if cleanup != nil {
cleanup ()
}
}()
if err != nil {
c . PrintCliError ( err )
return base . CommandCliError
}
if ! continueRun {
return base . CommandSuccess
}
// TODO: print something out for the spawner to consume in case they can easily
// report if the daemon started or not.
2024-01-22 08:42:19 -08:00
lf , logFileName , err := logFile ( ctx , dotDir , 5 )
2023-06-11 20:40:47 -04:00
if err != nil {
c . PrintCliError ( err )
return base . CommandCliError
}
2023-11-03 14:59:42 -07:00
defer lf . Close ()
writers = append ( writers , lf )
2023-06-11 20:40:47 -04:00
2024-02-22 10:20:48 -08:00
if c . flagStoreDebug {
c . UI . Warn ( "The -store-debug flag is now ignored. Use -log-level=debug instead for debugging purposes." )
}
2023-06-11 20:40:47 -04:00
cfg := & daemon . Config {
2024-01-29 15:38:45 -08:00
ContextCancel : cancel ,
RefreshInterval : c . flagRefreshInterval ,
RecheckSupportInterval : c . flagRecheckSupportInterval ,
MaxSearchStaleness : c . flagMaxSearchStaleness ,
MaxSearchRefreshTimeout : c . flagMaxSearchRefreshTimeout ,
DatabaseUrl : c . flagDatabaseUrl ,
LogLevel : c . flagLogLevel ,
LogFormat : c . flagLogFormat ,
LogWriter : io . MultiWriter ( writers ... ),
LogFileName : logFileName ,
DotDirectory : dotDir ,
RunningInBackground : os . Getenv ( backgroundEnvName ) == backgroundEnvVal ,
2023-06-11 20:40:47 -04:00
}
srv , err := daemon . New ( ctx , cfg )
if err != nil {
c . UI . Error ( err . Error ())
return base . CommandUserError
}
var srvErr error
var wg sync . WaitGroup
wg . Add ( 1 )
go func () {
defer wg . Done ()
srvErr = srv . Serve ( ctx , c )
}()
// This is a blocking call. We rely on the c.ShutdownCh to cancel this
// context when sigterm or sigint is received.
<- ctx . Done ()
if err := srv . Shutdown ( ctx ); err != nil {
c . PrintCliError ( err )
return base . CommandCliError
}
wg . Wait ()
if srvErr != nil {
c . PrintCliError ( srvErr )
return base . CommandCliError
}
return base . CommandSuccess
}
// DefaultDotDirectory returns the default path to the boundary dot directory.
func DefaultDotDirectory ( ctx context . Context ) ( string , error ) {
2024-05-22 11:11:48 -07:00
const op = "cache.DefaultDotDirectory"
2023-06-11 20:40:47 -04:00
homeDir , err := homedir . Dir ()
if err != nil {
return "" , errors . Wrap ( ctx , err , op )
}
return filepath . Join ( homeDir , dotDirname ), nil
}
2023-11-03 14:59:42 -07:00
// logFile returns a log file which is rotated after it reaches the provided
// maximum size in mb before being rotated out. The rotated out log file gets
// a suffix that matches the time that the rotation happened. Up to 3 log files
// are saved as backup. When a new log file is rotated and there is already 3
// backups created, the oldest one is deleted.
2024-01-22 08:42:19 -08:00
func logFile ( ctx context . Context , dotDir string , maxSizeMb int ) ( io . WriteCloser , string , error ) {
2024-05-22 11:11:48 -07:00
const op = "cache.logFile"
2023-11-03 14:59:42 -07:00
logFilePath := filepath . Join ( dotDir , logFileName )
{
// Ensure the file is created with the desired permissions.
logFile , err := os . OpenFile ( logFilePath , os . O_CREATE | os . O_WRONLY , 0 o600 )
if err != nil {
2024-01-22 08:42:19 -08:00
return nil , "" , errors . Wrap ( ctx , err , op )
2023-11-03 14:59:42 -07:00
}
logFile . Close ()
}
logFile := & lumberjack . Logger {
Filename : logFilePath ,
MaxSize : maxSizeMb ,
MaxBackups : 3 ,
Compress : true ,
}
2024-01-22 08:42:19 -08:00
return logFile , logFilePath , nil
2023-11-03 14:59:42 -07:00
}
2023-11-30 08:20:57 -08:00
func ( c * StartCommand ) makeBackground ( ctx context . Context , dotDir string ) ( bool , [] io . Writer , pidCleanup , error ) {
2024-05-22 11:11:48 -07:00
const op = "cache.makeBackground"
2023-06-11 20:40:47 -04:00
writers := [] io . Writer {}
pidPath := filepath . Join ( dotDir , pidFileName )
if running , err := pidFileInUse ( ctx , pidPath ); running != nil {
2024-05-22 11:11:48 -07:00
return false , writers , noopPidCleanup , stderrors . New ( "The cache is already running." )
2023-06-11 20:40:47 -04:00
} else if err != nil && ! errors . Match ( errors . T ( errors . NotFound ), err ) {
2024-05-22 11:11:48 -07:00
return false , writers , noopPidCleanup , fmt . Errorf ( "Error when checking if the cache pid is in use: %w." , err )
2023-06-11 20:40:47 -04:00
}
2023-11-30 08:20:57 -08:00
if ! c . flagBackground && os . Getenv ( backgroundEnvName ) != backgroundEnvVal {
2023-06-11 20:40:47 -04:00
writers = append ( writers , os . Stderr )
}
2023-11-30 08:20:57 -08:00
if ! c . flagBackground || os . Getenv ( backgroundEnvName ) == backgroundEnvVal {
2023-06-11 20:40:47 -04:00
// We are either already running in the background or background was
// not requested. Write the pid file and continue.
cleanup , err := writePidFile ( ctx , pidPath )
if err != nil {
return false , writers , noopPidCleanup , errors . Wrap ( ctx , err , op )
}
return true , writers , cleanup , nil
}
absPath , err := os . Executable ()
if err != nil {
return false , writers , noopPidCleanup , errors . Wrap ( ctx , err , op )
}
env := os . Environ ()
env = append ( env , fmt . Sprintf ( "%s=%s" , backgroundEnvName , backgroundEnvVal ))
2024-05-22 11:11:48 -07:00
args := [] string { "cache" , "start" }
2023-12-20 11:07:38 -08:00
args = append ( args , "-refresh-interval" , c . flagRefreshInterval . String ())
args = append ( args , "-max-search-staleness" , c . flagMaxSearchStaleness . String ())
args = append ( args , "-max-search-refresh-timeout" , c . flagMaxSearchRefreshTimeout . String ())
args = append ( args , "-recheck-support-interval" , c . flagRecheckSupportInterval . String ())
2023-11-30 08:20:57 -08:00
if c . flagLogLevel != "" {
args = append ( args , "-log-level" , c . flagLogLevel )
}
if c . flagLogFormat != "" {
args = append ( args , "-log-format" , c . flagLogFormat )
}
if c . flagDatabaseUrl != "" {
args = append ( args , "-database-url" , c . flagDatabaseUrl )
}
cmd := exec . Command ( absPath , args ... )
2023-06-11 20:40:47 -04:00
cmd . Env = env
if err = cmd . Start (); err != nil {
return false , writers , noopPidCleanup , errors . Wrap ( ctx , err , op )
}
// TODO: Read the output from the child process for a brief time
// to see if we can identify any errors that might arise.
return false , writers , noopPidCleanup , nil
}
type pidCleanup func () error
var noopPidCleanup pidCleanup = func () error { return nil }