Rename daemon CLI command to cache (#4808)
* internal/cmd/wrapper: add deprecation wrapper The deprecation wrapper can be used to display a warning to callers when a command is deprecated. * all: rename daemon to cache Rename the boundary daemon command to boundary cache. This avoids confusion with future client/daemons. * changes daemon to cache in nav --------- Co-authored-by: Robin Beck <stellarsquall@protonmail.ch>
This commit is contained in:
1 parent
be7d72d690
commit
e64443c66f
34 files changed
+554
-299
No files matched your search
+213
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/client"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*AddTokenCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*AddTokenCommand)(nil)
|
||||
)
|
||||
|
||||
type AddTokenCommand struct {
|
||||
*base.Command
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) Synopsis() string {
|
||||
return "Add an auth token to a running boundary cache"
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary cache add-token [options]
|
||||
|
||||
Add an auth token to the cache:
|
||||
|
||||
$ boundary cache add-token
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetOutputFormat)
|
||||
f := set.NewFlagSet("Connection Options")
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: base.FlagNameAddr,
|
||||
Target: &c.FlagAddr,
|
||||
EnvVar: api.EnvBoundaryAddr,
|
||||
Completion: complete.PredictAnything,
|
||||
Usage: "Addr of the Boundary controller, as a complete URL (e.g. https://boundary.example.com:9200).",
|
||||
})
|
||||
|
||||
f = set.NewFlagSet("Client Options")
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "token-name",
|
||||
Target: &c.FlagTokenName,
|
||||
EnvVar: base.EnvTokenName,
|
||||
Usage: `If specified, the given value will be used as the name when storing the token in the system credential store. This can allow switching user identities for different commands.`,
|
||||
})
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "keyring-type",
|
||||
Target: &c.FlagKeyringType,
|
||||
Default: "auto",
|
||||
EnvVar: base.EnvKeyringType,
|
||||
Usage: `The type of keyring to use. Defaults to "auto" which will use the Windows credential manager, OSX keychain, or cross-platform password store depending on platform. Set to "none" to disable keyring functionality. Available types, depending on platform, are: "wincred", "keychain", "pass", and "secret-service".`,
|
||||
})
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "token",
|
||||
Target: &c.FlagToken,
|
||||
Usage: `A URL pointing to a file on disk (file://) from which a token will be read or an env var (env://) from which the token will be read. Overrides the "token-name" parameter.`,
|
||||
})
|
||||
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: "output-curl-string",
|
||||
Target: &c.FlagOutputCurlString,
|
||||
Usage: "Instead of executing the request, print an equivalent cURL command string and exit.",
|
||||
})
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *AddTokenCommand) Run(args []string) int {
|
||||
ctx := c.Context
|
||||
f := c.Flags()
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
// We ignore the error here since not having a keyring on the platform
|
||||
// returns an error but can be treated as the keyring type being set to
|
||||
// none.
|
||||
keyringType, tokenName, _ := c.DiscoverKeyringTokenInfo()
|
||||
|
||||
resp, apiErr, err := c.Add(ctx, c.UI, client, keyringType, tokenName)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
if apiErr != nil {
|
||||
c.PrintApiError(apiErr, "Error from cache when adding a token")
|
||||
return base.CommandApiError
|
||||
}
|
||||
switch base.Format(c.UI) {
|
||||
case "json":
|
||||
if ok := c.PrintJsonItem(resp); !ok {
|
||||
return base.CommandCliError
|
||||
}
|
||||
case "table":
|
||||
c.UI.Output("The cache add-token operation completed successfully.")
|
||||
}
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
// Add builds the UpsertTokenRequest using the client's address and token,
|
||||
// and trying to leverage the keyring. It then sends the request to the cache.
|
||||
// The passed in cli.Ui is used to print out any errors when looking up the
|
||||
// auth token from the keyring. This allows background operations calling this
|
||||
// method to pass in a silent UI to suppress any output.
|
||||
func (c *AddTokenCommand) Add(ctx context.Context, ui cli.Ui, apiClient *api.Client, keyringType, tokenName string) (*api.Response, *api.Error, error) {
|
||||
pa := daemon.UpsertTokenRequest{
|
||||
BoundaryAddr: apiClient.Addr(),
|
||||
}
|
||||
token := apiClient.Token()
|
||||
if token == "" {
|
||||
return nil, nil, errors.New("The client auth token is empty.")
|
||||
}
|
||||
|
||||
if parts := strings.SplitN(token, "_", 4); len(parts) == 3 {
|
||||
pa.AuthTokenId = strings.Join(parts[:2], "_")
|
||||
} else {
|
||||
return nil, nil, errors.New("The client provided auth token is not in the proper format.")
|
||||
}
|
||||
var opts []client.Option
|
||||
if c.FlagOutputCurlString {
|
||||
opts = append(opts, client.WithOutputCurlString())
|
||||
pa.AuthToken = "/*token*/"
|
||||
} else {
|
||||
pa.AuthToken = token
|
||||
}
|
||||
|
||||
switch keyringType {
|
||||
case "", base.NoneKeyring:
|
||||
// we don't need to do anything else in this situation since the keyring
|
||||
// doesn't play a part in the request.
|
||||
default:
|
||||
// Just because the keyring type is set doesn't mean the token to add
|
||||
// is contained in it. For example, if the token was intercepted
|
||||
// from an authentication request with '-format json' then token is
|
||||
// not stored in the keyring, even if a keyring is provided.
|
||||
|
||||
// Try to read the token from the keyring in a best effort way. Ignore
|
||||
// any errors since the keyring may not be present on the system.
|
||||
at := base.ReadTokenFromKeyring(ui, keyringType, tokenName)
|
||||
if at != nil && (token == "" || pa.AuthTokenId == at.Id) {
|
||||
pa.Keyring = &daemon.KeyringToken{
|
||||
KeyringType: keyringType,
|
||||
TokenName: tokenName,
|
||||
}
|
||||
// since the auth token is stored in the keyring, we can share it
|
||||
// through the keyring instead of passing it through the request.
|
||||
pa.AuthToken = ""
|
||||
}
|
||||
}
|
||||
|
||||
dotPath, err := DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return addToken(ctx, dotPath, &pa, opts...)
|
||||
}
|
||||
|
||||
func addToken(ctx context.Context, daemonPath string, p *daemon.UpsertTokenRequest, opt ...client.Option) (*api.Response, *api.Error, error) {
|
||||
addr := daemon.SocketAddress(daemonPath)
|
||||
_, err := os.Stat(addr.Path)
|
||||
if addr.Scheme == "unix" && err != nil {
|
||||
return nil, nil, fmt.Errorf("Error when detecting if the domain socket is present: %w.", err)
|
||||
}
|
||||
|
||||
c, err := client.New(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Error when making a new client: %w.", err)
|
||||
}
|
||||
resp, err := c.Post(ctx, "/v1/tokens", p, opt...)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Error when sending request to the cache: %w.", err)
|
||||
}
|
||||
apiErr, err := resp.Decode(nil)
|
||||
return resp, apiErr, err
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*StartCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*StartCommand)(nil)
|
||||
)
|
||||
|
||||
type CacheCommand struct {
|
||||
*base.Command
|
||||
}
|
||||
|
||||
func (c *CacheCommand) Synopsis() string {
|
||||
return "Manages the client side Boundary cache"
|
||||
}
|
||||
|
||||
func (c *CacheCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary cache [sub command] [options]
|
||||
|
||||
This command allows interacting with the Boundary cache.
|
||||
|
||||
Start a cache:
|
||||
|
||||
$ boundary cache start
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *CacheCommand) Flags() *base.FlagSets {
|
||||
return c.FlagSet(base.FlagSetNone)
|
||||
}
|
||||
|
||||
func (c *CacheCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *CacheCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *CacheCommand) Run(args []string) int {
|
||||
if len(args) >= 2 && args[1] == "daemon" {
|
||||
c.UI.Warn("The `boundary daemon` command is deprecated, use `boundary cache` instead")
|
||||
}
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/event"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWithEventer(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dir := t.TempDir()
|
||||
l, loc, err := logFile(ctx, dir, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(dir, logFileName), loc)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, l.Close())
|
||||
})
|
||||
|
||||
var logLock sync.Mutex
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Output: l,
|
||||
Level: hclog.Debug,
|
||||
JSONFormat: false,
|
||||
Mutex: &logLock,
|
||||
})
|
||||
require.NoError(t, event.InitFallbackLogger(logger))
|
||||
|
||||
cfg := event.EventerConfig{
|
||||
AuditEnabled: false,
|
||||
ObservationsEnabled: true,
|
||||
SysEventsEnabled: true,
|
||||
Sinks: []*event.SinkConfig{
|
||||
{
|
||||
Name: "default",
|
||||
EventTypes: []event.Type{event.EveryType},
|
||||
Format: event.JSONHclogSinkFormat,
|
||||
Type: event.WriterSink,
|
||||
WriterConfig: &event.WriterSinkTypeConfig{
|
||||
Writer: l,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, event.InitSysEventer(logger, &logLock, "test", event.WithEventerConfig(&cfg)))
|
||||
|
||||
// Measured to have at least 1 record written in a rotated log file.
|
||||
for i := 0; i < 8129; i++ {
|
||||
event.WriteSysEvent(ctx, "test.caller", "this is a test event I am writing out.")
|
||||
}
|
||||
require.NoError(t, l.Close())
|
||||
ret, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, len(ret), 2)
|
||||
|
||||
t.Run("new logfile valid event", func(t *testing.T) {
|
||||
out, err := os.ReadFile(filepath.Join(dir, logFileName))
|
||||
require.NoError(t, err)
|
||||
t.Logf("Got output of length %d", len(out))
|
||||
jd := json.NewDecoder(bytes.NewReader(out))
|
||||
m := make(map[string]interface{})
|
||||
assert.NoError(t, jd.Decode(&m), fmt.Sprintf("parsing %q", out))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRotation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dir := t.TempDir()
|
||||
l, loc, err := logFile(ctx, dir, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(dir, logFileName), loc)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, l.Close())
|
||||
})
|
||||
|
||||
fi, err := os.Stat(filepath.Join(dir, logFileName))
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, fi.Size())
|
||||
assert.Equal(t, fs.FileMode(0o600), fi.Mode())
|
||||
|
||||
// write 1 mb and see it all contained in a single log file
|
||||
toWrite := make([]byte, 1024)
|
||||
for i := 0; i < 1024; i++ {
|
||||
_, err := rand.Read(toWrite)
|
||||
require.NoError(t, err)
|
||||
_, err = l.Write(toWrite)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
ret, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ret, 1)
|
||||
assert.Equal(t, ret[0].Name(), logFileName)
|
||||
|
||||
// write 1 more byte and see that it caused the log file to rotate
|
||||
_, err = l.Write([]byte("1"))
|
||||
require.NoError(t, err)
|
||||
|
||||
ret, err = os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
// GreaterOrEqual is used here because while backups are compressed, both
|
||||
// the renamed backup .log and the log.gz files exist
|
||||
require.GreaterOrEqual(t, len(ret), 2)
|
||||
assert.True(t, strings.HasPrefix(ret[0].Name(), "cache"))
|
||||
assert.True(t, strings.HasPrefix(ret[1].Name(), "cache"))
|
||||
|
||||
// write another 1 mb and see a third log file is created
|
||||
for i := 0; i < 1024; i++ {
|
||||
_, err := rand.Read(toWrite)
|
||||
require.NoError(t, err)
|
||||
_, err = l.Write(toWrite)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
ret, err = os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, len(ret), 3)
|
||||
|
||||
// and a 4th (3 backups plus the cache.log file)
|
||||
for i := 0; i < 1024; i++ {
|
||||
_, err := rand.Read(toWrite)
|
||||
require.NoError(t, err)
|
||||
_, err = l.Write(toWrite)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Rotation causes the log files to be compressed after they are rotated.
|
||||
// If the test ends before all the files have been compressed and the old
|
||||
// .log files removed then cleanup fails with an error.
|
||||
for {
|
||||
ret, err = os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, len(ret), 4)
|
||||
|
||||
filtered := slices.DeleteFunc(ret, func(f fs.DirEntry) bool {
|
||||
return strings.HasSuffix(f.Name(), "cache.log") ||
|
||||
strings.HasSuffix(f.Name(), ".gz")
|
||||
})
|
||||
if len(filtered) == 0 {
|
||||
// Indicates all the rotated log files have finished being
|
||||
// compressed and can now be removed without new files being
|
||||
// written to the directory.
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/sevlyar/go-daemon"
|
||||
)
|
||||
|
||||
func writePidFile(ctx context.Context, pidFile string) (pidCleanup, error) {
|
||||
const op = "cache.writePidFile"
|
||||
|
||||
// Determine if we should clean up the file after we are done or if
|
||||
// it should stick around in the case of lock aquision error since this
|
||||
// file didn't create it.
|
||||
var pidExists bool
|
||||
_, err := os.Stat(pidFile)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return noopPidCleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if err == nil {
|
||||
pidExists = true
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(pidFile, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return noopPidCleanup, errors.Wrap(ctx, err, op, errors.WithMsg("opening file"))
|
||||
}
|
||||
closeAndDeleteFileFn := func() error {
|
||||
err := f.Close()
|
||||
if !pidExists {
|
||||
err = stderrors.Join(err, os.Remove(pidFile))
|
||||
}
|
||||
return err
|
||||
}
|
||||
l := daemon.NewLockFile(f)
|
||||
if err := l.Lock(); err != nil {
|
||||
return closeAndDeleteFileFn, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
// Now that we have acquired the lock and verified we own the pid file
|
||||
// we can remove it always when cleaning up.
|
||||
unlockAndCleanFn := func() error {
|
||||
return l.Remove()
|
||||
}
|
||||
if err := l.WritePid(); err != nil {
|
||||
return unlockAndCleanFn, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return unlockAndCleanFn, nil
|
||||
}
|
||||
|
||||
func pidFileInUse(ctx context.Context, pidFile string) (*os.Process, error) {
|
||||
const op = "cache.pidFileInUse"
|
||||
if pidFile == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "pid filename is empty")
|
||||
}
|
||||
proc, err := (&daemon.Context{PidFileName: pidFile}).Search()
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
return proc, nil
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPidInUse(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dotPath := t.TempDir()
|
||||
pidPath := filepath.Join(dotPath, "boundary.pid")
|
||||
|
||||
used, err := pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, used)
|
||||
|
||||
workingPidCleanup1, err := writePidFile(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, used)
|
||||
|
||||
failingPidCleanup, err := writePidFile(ctx, pidPath)
|
||||
assert.Error(t, err)
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, used)
|
||||
|
||||
assert.NoError(t, failingPidCleanup())
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, used)
|
||||
|
||||
assert.NoError(t, workingPidCleanup1())
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, used)
|
||||
|
||||
workingPidCleanup2, err := writePidFile(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, used)
|
||||
|
||||
assert.NoError(t, workingPidCleanup2())
|
||||
|
||||
used, err = pidFileInUse(ctx, pidPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, used)
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func writePidFile(ctx context.Context, pidFile string) (pidCleanup, error) {
|
||||
const op = "cache.writePidFile"
|
||||
// Create the file for writing, and set shared read so following processes
|
||||
// cannot open this file for writing.
|
||||
fd, err := windows.CreateFile(&(windows.StringToUTF16(pidFile)[0]), windows.GENERIC_WRITE,
|
||||
windows.FILE_SHARE_READ, nil, windows.OPEN_ALWAYS, windows.FILE_ATTRIBUTE_NORMAL, 0)
|
||||
if err != nil {
|
||||
return noopPidCleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
cleanup := func() error {
|
||||
var err error
|
||||
if inErr := windows.CloseHandle(fd); inErr != nil {
|
||||
err = stderrors.Join(err, errors.Wrap(ctx, inErr, op, errors.WithMsg("handler close")))
|
||||
}
|
||||
if inErr := windows.DeleteFile(&(windows.StringToUTF16(pidFile)[0])); inErr != nil {
|
||||
err = stderrors.Join(err, errors.Wrap(ctx, inErr, op, errors.WithMsg("removing file")))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := windows.Seek(fd, 0, windows.FILE_BEGIN); err != nil {
|
||||
return cleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
b := bytes.NewBuffer(nil)
|
||||
if _, err := fmt.Fprint(b, os.Getpid()); err != nil {
|
||||
return cleanup, errors.Wrap(ctx, err, op, errors.WithMsg("writing file buffer"))
|
||||
}
|
||||
var fileLen int
|
||||
if fileLen, err = windows.Write(fd, b.Bytes()); err != nil {
|
||||
return cleanup, errors.Wrap(ctx, err, op, errors.WithMsg("writing buffer to file"))
|
||||
}
|
||||
if err = windows.Ftruncate(fd, int64(fileLen)); err != nil {
|
||||
return cleanup, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return cleanup, windows.Fsync(fd)
|
||||
}
|
||||
|
||||
func pidFileInUse(ctx context.Context, pidFile string) (*os.Process, error) {
|
||||
const op = "cache.pidFileInUse"
|
||||
if pidFile == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "pid filename is empty")
|
||||
}
|
||||
|
||||
var err error
|
||||
var file *os.File
|
||||
if file, err = os.OpenFile(pidFile, os.O_RDONLY, 0o640); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("OpenFile"))
|
||||
}
|
||||
if file == nil {
|
||||
return nil, nil
|
||||
}
|
||||
defer func() {
|
||||
file.Close()
|
||||
}()
|
||||
|
||||
if _, err = file.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pid int
|
||||
_, err = fmt.Fscan(file, &pid)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("Fscan"))
|
||||
}
|
||||
|
||||
p, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "The parameter is incorrect") {
|
||||
return nil, errors.New(ctx, errors.NotFound, op, "cannot find process")
|
||||
}
|
||||
// we failed to get the process for whatever reason
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("FindProcess %d", pid))
|
||||
}
|
||||
if p == nil {
|
||||
return nil, errors.New(ctx, errors.NotFound, op, "cannot find process")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
dotDirname = ".boundary"
|
||||
pidFileName = "cache.pid"
|
||||
logFileName = "cache.log"
|
||||
|
||||
// Mark of process as having been started in the background
|
||||
backgroundEnvName = "_BOUNDARY_CACHE_BACKGROUND"
|
||||
backgroundEnvVal = "1"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*StartCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*StartCommand)(nil)
|
||||
)
|
||||
|
||||
type StartCommand struct {
|
||||
*base.Command
|
||||
|
||||
flagRefreshInterval time.Duration
|
||||
flagRecheckSupportInterval time.Duration
|
||||
flagMaxSearchStaleness time.Duration
|
||||
flagMaxSearchRefreshTimeout time.Duration
|
||||
flagDatabaseUrl string
|
||||
flagLogLevel string
|
||||
flagLogFormat string
|
||||
flagStoreDebug bool
|
||||
flagBackground bool
|
||||
}
|
||||
|
||||
func (c *StartCommand) Synopsis() string {
|
||||
return "Start a Boundary cache"
|
||||
}
|
||||
|
||||
func (c *StartCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary cache start [options]
|
||||
|
||||
Start a cache:
|
||||
|
||||
$ boundary cache start
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *StartCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetNone)
|
||||
|
||||
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.`,
|
||||
Hidden: true,
|
||||
})
|
||||
f.DurationVar(&base.DurationVar{
|
||||
Name: "refresh-interval",
|
||||
Target: &c.flagRefreshInterval,
|
||||
Usage: `Specifies the interval between refresh token supported cache refreshes.`,
|
||||
Default: daemon.DefaultRefreshInterval,
|
||||
})
|
||||
f.DurationVar(&base.DurationVar{
|
||||
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,
|
||||
})
|
||||
f.DurationVar(&base.DurationVar{
|
||||
Name: "max-search-staleness",
|
||||
Target: &c.flagMaxSearchStaleness,
|
||||
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.`,
|
||||
Default: daemon.DefaultSearchStaleness,
|
||||
})
|
||||
f.DurationVar(&base.DurationVar{
|
||||
Name: "max-search-refresh-timeout",
|
||||
Target: &c.flagMaxSearchRefreshTimeout,
|
||||
Usage: `If a search request triggers a best effort refresh, this specifies how long the refresh should run before timing out.`,
|
||||
Default: daemon.DefaultSearchRefreshTimeout,
|
||||
})
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: "store-debug",
|
||||
Target: &c.flagStoreDebug,
|
||||
Default: false,
|
||||
Usage: `Turn on sqlite query debugging. This is deprecated. Users should use -log-level=debug instead.`,
|
||||
Aliases: []string{"d"},
|
||||
Hidden: true,
|
||||
})
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: "background",
|
||||
Target: &c.flagBackground,
|
||||
Default: false,
|
||||
Usage: `Run the cache daemon in the background`,
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
if err := os.MkdirAll(dotDir, 0o700); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
continueRun, writers, cleanup, err := c.makeBackground(ctx, dotDir)
|
||||
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.
|
||||
|
||||
lf, logFileName, err := logFile(ctx, dotDir, 5)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
defer lf.Close()
|
||||
writers = append(writers, lf)
|
||||
|
||||
if c.flagStoreDebug {
|
||||
c.UI.Warn("The -store-debug flag is now ignored. Use -log-level=debug instead for debugging purposes.")
|
||||
}
|
||||
|
||||
cfg := &daemon.Config{
|
||||
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,
|
||||
}
|
||||
|
||||
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) {
|
||||
const op = "cache.DefaultDotDirectory"
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return filepath.Join(homeDir, dotDirname), nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func logFile(ctx context.Context, dotDir string, maxSizeMb int) (io.WriteCloser, string, error) {
|
||||
const op = "cache.logFile"
|
||||
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, 0o600)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
logFile.Close()
|
||||
}
|
||||
|
||||
logFile := &lumberjack.Logger{
|
||||
Filename: logFilePath,
|
||||
MaxSize: maxSizeMb,
|
||||
MaxBackups: 3,
|
||||
Compress: true,
|
||||
}
|
||||
return logFile, logFilePath, nil
|
||||
}
|
||||
|
||||
func (c *StartCommand) makeBackground(ctx context.Context, dotDir string) (bool, []io.Writer, pidCleanup, error) {
|
||||
const op = "cache.makeBackground"
|
||||
|
||||
writers := []io.Writer{}
|
||||
pidPath := filepath.Join(dotDir, pidFileName)
|
||||
if running, err := pidFileInUse(ctx, pidPath); running != nil {
|
||||
return false, writers, noopPidCleanup, stderrors.New("The cache is already running.")
|
||||
} else if err != nil && !errors.Match(errors.T(errors.NotFound), err) {
|
||||
return false, writers, noopPidCleanup, fmt.Errorf("Error when checking if the cache pid is in use: %w.", err)
|
||||
}
|
||||
|
||||
if !c.flagBackground && os.Getenv(backgroundEnvName) != backgroundEnvVal {
|
||||
writers = append(writers, os.Stderr)
|
||||
}
|
||||
|
||||
if !c.flagBackground || os.Getenv(backgroundEnvName) == backgroundEnvVal {
|
||||
// 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))
|
||||
args := []string{"cache", "start"}
|
||||
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())
|
||||
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...)
|
||||
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 }
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderr "errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/client"
|
||||
"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/posener/complete"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var errCacheNotRunning = stderr.New("The cache process is not running.")
|
||||
|
||||
var (
|
||||
_ cli.Command = (*AddTokenCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*AddTokenCommand)(nil)
|
||||
)
|
||||
|
||||
type StatusCommand struct {
|
||||
*base.Command
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Synopsis() string {
|
||||
return "Get the status information of the running boundary cache"
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary cache status [options]
|
||||
|
||||
Get the status of the boundary cache:
|
||||
|
||||
$ boundary cache status
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetOutputFormat)
|
||||
f := set.NewFlagSet("Client Options")
|
||||
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: "output-curl-string",
|
||||
Target: &c.FlagOutputCurlString,
|
||||
Usage: "Instead of executing the request, print an equivalent cURL command string and exit.",
|
||||
})
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *StatusCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *StatusCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Run(args []string) int {
|
||||
ctx := c.Context
|
||||
f := c.Flags()
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
resp, result, apiErr, err := c.Status(ctx)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
if apiErr != nil {
|
||||
c.PrintApiError(apiErr, "Error from cache when getting the status")
|
||||
return base.CommandApiError
|
||||
}
|
||||
|
||||
switch base.Format(c.UI) {
|
||||
case "json":
|
||||
if ok := c.PrintJsonItem(resp); !ok {
|
||||
return base.CommandCliError
|
||||
}
|
||||
default:
|
||||
c.UI.Output(printStatusTable(result))
|
||||
}
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Status(ctx context.Context) (*api.Response, *daemon.StatusResult, *api.Error, error) {
|
||||
dotPath, err := DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
var opts []client.Option
|
||||
if c.FlagOutputCurlString {
|
||||
opts = append(opts, client.WithOutputCurlString())
|
||||
}
|
||||
|
||||
return status(ctx, dotPath, opts...)
|
||||
}
|
||||
|
||||
func status(ctx context.Context, daemonPath string, opt ...client.Option) (*api.Response, *daemon.StatusResult, *api.Error, error) {
|
||||
const op = "cache.status"
|
||||
addr := daemon.SocketAddress(daemonPath)
|
||||
_, err := os.Stat(addr.Path)
|
||||
if addr.Scheme == "unix" && err != nil {
|
||||
return nil, nil, nil, errCacheNotRunning
|
||||
}
|
||||
c, err := client.New(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
resp, err := c.Get(ctx, "/v1/status", nil, opt...)
|
||||
if err != nil {
|
||||
return nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("client do failed"))
|
||||
}
|
||||
|
||||
res := &daemon.StatusResult{}
|
||||
apiErr, err := resp.Decode(&res)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("Error when sending request to the cache: %w.", err)
|
||||
}
|
||||
if apiErr != nil {
|
||||
return resp, nil, apiErr, nil
|
||||
}
|
||||
return resp, res, nil, nil
|
||||
}
|
||||
|
||||
func printStatusTable(status *daemon.StatusResult) string {
|
||||
nonAttributeMap := map[string]any{
|
||||
"User Count": len(status.Users),
|
||||
}
|
||||
if len(status.SocketAddress) > 0 {
|
||||
nonAttributeMap["Domain Socket"] = status.SocketAddress
|
||||
}
|
||||
if len(status.LogLocation) > 0 {
|
||||
nonAttributeMap["Log Location"] = status.LogLocation
|
||||
}
|
||||
if len(status.Version) > 0 {
|
||||
nonAttributeMap["Version"] = status.Version
|
||||
}
|
||||
if status.Uptime > 0 {
|
||||
nonAttributeMap["Uptime"] = status.Uptime.Round(time.Second)
|
||||
}
|
||||
|
||||
maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil)
|
||||
|
||||
ret := []string{
|
||||
"",
|
||||
"Status:",
|
||||
base.WrapMap(2, maxLength+2, nonAttributeMap),
|
||||
}
|
||||
|
||||
if len(status.Users) > 0 {
|
||||
ret = append(ret, printUsersTable(status.Users)...)
|
||||
}
|
||||
return base.WrapForHelpText(ret)
|
||||
}
|
||||
|
||||
func printUsersTable(us []daemon.UserStatus) []string {
|
||||
ret := []string{
|
||||
"",
|
||||
}
|
||||
for _, u := range us {
|
||||
ret = append(ret, " User:")
|
||||
nonAttributeMap := map[string]any{
|
||||
"Id": u.Id,
|
||||
"Address": u.BoundaryInstance.Address,
|
||||
"AuthToken Count": len(u.AuthTokens),
|
||||
"Search Support": u.BoundaryInstance.CacheSupport,
|
||||
}
|
||||
if u.BoundaryInstance.LastSupportCheck > 0 {
|
||||
nonAttributeMap["Since Search Support Check"] = u.BoundaryInstance.LastSupportCheck.Round(time.Second)
|
||||
}
|
||||
maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil)
|
||||
ret = append(ret,
|
||||
base.WrapMap(4, maxLength+4, nonAttributeMap),
|
||||
)
|
||||
|
||||
for _, at := range u.AuthTokens {
|
||||
nonAttributeMap := map[string]any{
|
||||
"Id": at.Id,
|
||||
}
|
||||
if at.KeyringReferences > 0 {
|
||||
nonAttributeMap["In Keyring"] = "true"
|
||||
}
|
||||
maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil)
|
||||
ret = append(ret,
|
||||
" AuthToken:",
|
||||
base.WrapMap(6, maxLength+6, nonAttributeMap),
|
||||
)
|
||||
}
|
||||
|
||||
for _, r := range u.Resources {
|
||||
nonAttributeMap := map[string]any{
|
||||
"Count": r.Count,
|
||||
}
|
||||
if r.RefreshToken != nil {
|
||||
nonAttributeMap["Since Last Refresh"] = r.RefreshToken.LastUsed.Round(time.Second)
|
||||
nonAttributeMap["Since Initial Fetch"] = r.RefreshToken.Age.Round(time.Second)
|
||||
}
|
||||
if r.LastError != nil {
|
||||
nonAttributeMap["Since Last Error"] = r.LastError.LastReturned.Round(time.Second)
|
||||
nonAttributeMap["Last Error Message"] = r.LastError.Error
|
||||
}
|
||||
maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil)
|
||||
ret = append(ret,
|
||||
fmt.Sprintf(" %s:", cases.Title(language.English).String(r.Name)),
|
||||
base.WrapMap(6, maxLength+6, nonAttributeMap),
|
||||
)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type StopCommand struct {
|
||||
*base.Command
|
||||
}
|
||||
|
||||
func (c *StopCommand) Synopsis() string {
|
||||
return "Stop the Boundary cache"
|
||||
}
|
||||
|
||||
func (c *StopCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary cache stop
|
||||
|
||||
Stop a cache:
|
||||
|
||||
$ boundary cache stop
|
||||
|
||||
For a full list of examples, please see the documentation.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *StopCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetNone)
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *StopCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *StopCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *StopCommand) Run(args []string) int {
|
||||
ctx, cancel := context.WithCancel(c.Context)
|
||||
c.Context = ctx
|
||||
c.ContextCancel = cancel
|
||||
|
||||
f := c.Flags()
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
if err := c.stop(c.Context); err != nil {
|
||||
if errors.Match(errors.T(errors.NotFound), err) {
|
||||
c.PrintCliError(errCacheNotRunning)
|
||||
} else {
|
||||
c.PrintCliError(err)
|
||||
}
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
return base.CommandSuccess
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
// stop will send a term signal to the cache to shut down.
|
||||
func (c *StopCommand) stop(ctx context.Context) error {
|
||||
switch {
|
||||
case util.IsNil(ctx):
|
||||
return errors.New("Invalid parameter provided to stop: context is missing")
|
||||
}
|
||||
|
||||
dotPath, err := DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error when getting default dot directory: %w", err)
|
||||
}
|
||||
pidPath := filepath.Join(dotPath, pidFileName)
|
||||
p, err := pidFileInUse(ctx, pidPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error when checking if the cache's pid file is in use: %w", err)
|
||||
}
|
||||
if p == nil {
|
||||
return errors.New("The cache is not running.")
|
||||
}
|
||||
if err := p.Signal(syscall.SIGTERM); err != nil {
|
||||
return fmt.Errorf("Error when sending sigterm to cache process: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/client"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
// stop will send a term signal to the cache to shut down.
|
||||
func (c *StopCommand) stop(ctx context.Context) error {
|
||||
switch {
|
||||
case util.IsNil(ctx):
|
||||
return errors.New("Invalid parameter provided to stop: context is missing")
|
||||
}
|
||||
|
||||
dotPath, err := DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error when getting default dot directory: %w", err)
|
||||
}
|
||||
|
||||
apiErr, err := stopThroughHandler(ctx, dotPath)
|
||||
switch {
|
||||
case err != nil, apiErr != nil:
|
||||
var errMsg string
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
} else if apiErr != nil {
|
||||
errMsg = apiErr.Message
|
||||
}
|
||||
c.UI.Warn(fmt.Sprintf("Failed stopping the cache through the handler: %q, now killing the process", errMsg))
|
||||
default:
|
||||
// there wasn't an error stopping it through the handler. No need to
|
||||
// force kill the process
|
||||
return nil
|
||||
}
|
||||
|
||||
pidPath := filepath.Join(dotPath, pidFileName)
|
||||
p, err := pidFileInUse(ctx, pidPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errors.New("Unable to stop the cache: pid file not found.")
|
||||
}
|
||||
return fmt.Errorf("Error when trying to stop the cache: %w", err)
|
||||
}
|
||||
if p == nil {
|
||||
return errors.New("Cache process was not found.")
|
||||
}
|
||||
|
||||
if err := p.Kill(); err != nil {
|
||||
return fmt.Errorf("Error when killing the process: %w.", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopThroughHandler(ctx context.Context, dotPath string) (*api.Error, error) {
|
||||
addr := daemon.SocketAddress(dotPath)
|
||||
c, err := client.New(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.Post(ctx, "/v1/stop", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error when sending request to the cache: %w.", err)
|
||||
}
|
||||
apiErr, err := resp.Decode(nil)
|
||||
return apiErr, err
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/cmd/wrapper"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := wrapper.RegisterSuccessfulCommandCallback("clientcache", hook); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// hook is the callback that is registered with the wrapper package to be called.
|
||||
// The daemon is not started and the token is not added to the cache if the flag
|
||||
// SkipCacheDaemon is set.
|
||||
func hook(ctx context.Context, baseCmd *base.Command, token string) {
|
||||
if baseCmd.FlagSkipCacheDaemon {
|
||||
return
|
||||
}
|
||||
if startDaemon(ctx, baseCmd) {
|
||||
addTokenToCache(ctx, baseCmd, token)
|
||||
}
|
||||
}
|
||||
|
||||
// startDaemon attempts to start a daemon and returns true if we have attempted to start
|
||||
// the daemon and either it was successful or it was already running.
|
||||
func startDaemon(ctx context.Context, baseCmd *base.Command) bool {
|
||||
// Ignore errors related to checking if the process is already running since
|
||||
// this can fall back to running the process.
|
||||
if dotPath, err := DefaultDotDirectory(ctx); err == nil {
|
||||
pidPath := filepath.Join(dotPath, pidFileName)
|
||||
if running, _ := pidFileInUse(ctx, pidPath); running != nil {
|
||||
// return true since it is already running, no need to run it again.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
cmdName, err := os.Executable()
|
||||
if err != nil {
|
||||
baseCmd.UI.Error(fmt.Sprintf("unable to find boundary binary for cache startup: %s", err.Error()))
|
||||
return false
|
||||
}
|
||||
|
||||
var stdErr bytes.Buffer
|
||||
cmd := exec.Command(cmdName, "cache", "start", "-background")
|
||||
cmd.Stderr = &stdErr
|
||||
|
||||
// We use Run here instead of Start because the command spawns off a subprocess and returns.
|
||||
// We do not want to send the request to add a persona to the cache until we know the daemon
|
||||
// has started up.
|
||||
err = cmd.Run()
|
||||
return err == nil || strings.Contains(stdErr.String(), "already running")
|
||||
}
|
||||
|
||||
// silentUi should not be used in situations where the UI is expected to be
|
||||
// prompt the user for input.
|
||||
func silentUi() *cli.BasicUi {
|
||||
return &cli.BasicUi{
|
||||
Writer: io.Discard,
|
||||
ErrorWriter: io.Discard,
|
||||
}
|
||||
}
|
||||
|
||||
// addTokenToCache runs AddTokenCommand with the token used in, or retrieved by
|
||||
// the wrapped command.
|
||||
func addTokenToCache(ctx context.Context, baseCmd *base.Command, token string) bool {
|
||||
com := AddTokenCommand{Command: base.NewCommand(baseCmd.UI)}
|
||||
client, err := baseCmd.Client()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
keyringType, tokName, err := baseCmd.DiscoverKeyringTokenInfo()
|
||||
if err != nil && token == "" {
|
||||
return false
|
||||
}
|
||||
if token != "" {
|
||||
client.SetToken(token)
|
||||
}
|
||||
|
||||
// Since the daemon might have just started, we need to wait until it can
|
||||
// respond to our requests
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
if err := waitForDaemon(waitCtx); err != nil {
|
||||
// TODO: Print the result of this out into a log in the dot directory
|
||||
return false
|
||||
}
|
||||
|
||||
// We do not want to print errors out from our background interactions with
|
||||
// the daemon so use the silentUi to toss out anything that shouldn't be used
|
||||
_, apiErr, err := com.Add(ctx, silentUi(), client, keyringType, tokName)
|
||||
return err == nil && apiErr == nil
|
||||
}
|
||||
|
||||
// waitForDaemon continually looks for the unix socket until it is found or the
|
||||
// provided context is done. It returns an error if the unix socket is not found
|
||||
// before the context is done.
|
||||
func waitForDaemon(ctx context.Context) error {
|
||||
dotPath, err := DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
timer := time.NewTimer(0)
|
||||
|
||||
addr := daemon.SocketAddress(dotPath)
|
||||
_, err = os.Stat(addr.Path)
|
||||
for os.IsNotExist(err) {
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
_, err = os.Stat(addr.Path)
|
||||
timer.Reset(10 * time.Millisecond)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user