From c0b6976f76efae70a607dc8fb57c6ffc25f4d94c Mon Sep 17 00:00:00 2001 From: Todd Date: Wed, 2 Aug 2023 11:35:29 -0700 Subject: [PATCH] Move files to new directories and add persona table (#3526) * Add persona table and move files to new directories --- go.mod | 14 +- go.sum | 8 +- internal/cmd/commands.go | 16 +- .../{cache/cache.go => daemon/daemon.go} | 14 +- .../cmd/commands/{cache => daemon}/options.go | 2 +- .../cmd/commands/{cache => daemon}/search.go | 29 ++- .../cmd/commands/{cache => daemon}/server.go | 66 +++--- .../cmd/commands/{cache => daemon}/ticker.go | 34 ++- internal/cmd/main.go | 10 +- internal/{ => daemon}/cache/options.go | 32 ++- internal/{ => daemon}/cache/query.go | 0 internal/{ => daemon}/cache/repository.go | 186 +++++++++++++--- internal/daemon/cache/repository_test.go | 209 ++++++++++++++++++ internal/{ => daemon}/cache/store.go | 20 +- internal/daemon/cache/store_test.go | 129 +++++++++++ 15 files changed, 632 insertions(+), 137 deletions(-) rename internal/cmd/commands/{cache/cache.go => daemon/daemon.go} (95%) rename internal/cmd/commands/{cache => daemon}/options.go (97%) rename internal/cmd/commands/{cache => daemon}/search.go (91%) rename internal/cmd/commands/{cache => daemon}/server.go (89%) rename internal/cmd/commands/{cache => daemon}/ticker.go (74%) rename internal/{ => daemon}/cache/options.go (76%) rename internal/{ => daemon}/cache/query.go (100%) rename internal/{ => daemon}/cache/repository.go (52%) create mode 100644 internal/daemon/cache/repository_test.go rename internal/{ => daemon}/cache/store.go (69%) create mode 100644 internal/daemon/cache/store_test.go diff --git a/go.mod b/go.mod index ab0798a9f4..185bf3c28d 100644 --- a/go.mod +++ b/go.mod @@ -91,6 +91,8 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/cenkalti/backoff/v4 v4.2.0 github.com/creack/pty v1.1.18 + github.com/glebarez/go-sqlite v1.21.1 + github.com/glebarez/sqlite v1.8.0 github.com/hashicorp/cap/ldap v0.0.0-20230420150311-6d1e00a6c5e0 github.com/hashicorp/dbassert v0.0.0-20230405175854-2d88acd5134b github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20221122211539-47c893099f13 @@ -100,6 +102,10 @@ require ( github.com/jimlambrt/gldap v0.1.7 github.com/kelseyhightower/envconfig v1.4.0 github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a + github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/go-ps v1.0.0 + github.com/seldonio/goven v0.2.1 + github.com/sevlyar/go-daemon v0.1.6 golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 golang.org/x/net v0.10.0 google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc @@ -136,8 +142,6 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.5.0 // indirect - github.com/glebarez/go-sqlite v1.21.1 // indirect - github.com/glebarez/sqlite v1.8.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-ldap/ldap/v3 v3.4.4 // indirect @@ -169,11 +173,8 @@ require ( github.com/klauspost/compress v1.13.6 // indirect github.com/lib/pq v1.10.7 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect @@ -189,9 +190,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect - github.com/seldonio/goven v0.2.1 // indirect github.com/sethvargo/go-diceware v0.3.0 // indirect - github.com/sevlyar/go-daemon v0.1.6 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect @@ -210,7 +209,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/sqlite v1.5.1 // indirect modernc.org/libc v1.22.3 // indirect modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect diff --git a/go.sum b/go.sum index 1337c8dd26..2fa0b22955 100644 --- a/go.sum +++ b/go.sum @@ -622,6 +622,7 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210715191844-86eeefc3e471/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -678,8 +679,6 @@ github.com/hashicorp/go-bexpr v0.1.12/go.mod h1:ACktpcSySkFNpcxWSClFrut7wicd9Wzi github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-dbw v0.0.0-20230605211904-d40e5c9d5995 h1:2I5x7ULbjbQVxIvrVhBZU9gXXj/4UcApBuvKf5Rb3Ts= -github.com/hashicorp/go-dbw v0.0.0-20230605211904-d40e5c9d5995/go.mod h1:5bmfUkVf0I4E3tIftLJdBM8vw1Xqe7pmbeYBzdC1cj8= github.com/hashicorp/go-dbw v0.0.0-20230611160117-87377d5c7f5b h1:czvpsxAjufOAiO2ri2zK0Zb/kNYJfmQNu5y3lfXULxM= github.com/hashicorp/go-dbw v0.0.0-20230611160117-87377d5c7f5b/go.mod h1:zWbqyuJIlekgoweI8C6nyNRXTdLXkLK6Xqjag5jp0xA= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= @@ -1077,6 +1076,7 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1982,12 +1982,8 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= -gorm.io/driver/sqlite v1.5.1 h1:hYyrLkAWE71bcarJDPdZNTLWtr8XrSjOWyjUYI6xdL4= -gorm.io/driver/sqlite v1.5.1/go.mod h1:7MZZ2Z8bqyfSQA1gYEV6MagQWj3cpUkJj9Z+d1HEMEQ= gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= -gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64= -gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ= gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index e5b42f9bb0..ecb1362425 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -9,12 +9,12 @@ import ( "github.com/hashicorp/boundary/internal/cmd/commands/authenticate" "github.com/hashicorp/boundary/internal/cmd/commands/authmethodscmd" "github.com/hashicorp/boundary/internal/cmd/commands/authtokenscmd" - "github.com/hashicorp/boundary/internal/cmd/commands/cache" "github.com/hashicorp/boundary/internal/cmd/commands/config" "github.com/hashicorp/boundary/internal/cmd/commands/connect" "github.com/hashicorp/boundary/internal/cmd/commands/credentiallibrariescmd" "github.com/hashicorp/boundary/internal/cmd/commands/credentialscmd" "github.com/hashicorp/boundary/internal/cmd/commands/credentialstorescmd" + "github.com/hashicorp/boundary/internal/cmd/commands/daemon" "github.com/hashicorp/boundary/internal/cmd/commands/database" "github.com/hashicorp/boundary/internal/cmd/commands/dev" "github.com/hashicorp/boundary/internal/cmd/commands/groupscmd" @@ -364,13 +364,8 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { }, nil }, - "cache server": func() (cli.Command, error) { - return &cache.ServerCommand{ - Command: base.NewCommand(ui), - }, nil - }, - "cache search targets": func() (cli.Command, error) { - return &cache.SearchTargetsCommand{ + "daemon start": func() (cli.Command, error) { + return &daemon.ServerCommand{ Command: base.NewCommand(ui), }, nil }, @@ -1197,6 +1192,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "set-credential-sources", }, nil }, + "targets search": func() (cli.Command, error) { + return &daemon.SearchTargetsCommand{ + Command: base.NewCommand(ui), + }, nil + }, "users": func() (cli.Command, error) { return &userscmd.Command{ diff --git a/internal/cmd/commands/cache/cache.go b/internal/cmd/commands/daemon/daemon.go similarity index 95% rename from internal/cmd/commands/cache/cache.go rename to internal/cmd/commands/daemon/daemon.go index 044a58af43..a0c6c6cd3c 100644 --- a/internal/cmd/commands/cache/cache.go +++ b/internal/cmd/commands/daemon/daemon.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package cache +package daemon import ( "context" @@ -37,16 +37,16 @@ type ServerCommand struct { } func (c *ServerCommand) Synopsis() string { - return "Start a Boundary cache server" + return "Start a Boundary daemon" } func (c *ServerCommand) Help() string { helpText := ` -Usage: boundary cache server [options] +Usage: boundary daemon start [options] Start a cache server: - $ boundary cache server + $ boundary daemon start For a full list of examples, please see the documentation. @@ -80,7 +80,7 @@ func (c *ServerCommand) Flags() *base.FlagSets { f.Int64Var(&base.Int64Var{ Name: "refresh-interval-seconds", Target: &c.flagRefreshIntervalSeconds, - Usage: `If set, specifies the number of seconds between cache refreshes`, + Usage: `If set, specifies the number of seconds between cache refreshes. Default: 5 minutes`, Aliases: []string{"r"}, }) f.UintVar(&base.UintVar{ @@ -118,7 +118,7 @@ func (c *ServerCommand) AutocompleteFlags() complete.Flags { } func (c *ServerCommand) Run(args []string) int { - const op = "cache.(ServerCommand).Run" + const op = "daemon.(ServerCommand).Run" ctx, cancel := context.WithCancel(c.Context) c.Context = ctx c.ContextCancel = cancel @@ -162,7 +162,7 @@ func (c *ServerCommand) Run(args []string) int { const DefaultRefreshIntervalSeconds = 5 * 60 func StartCacheInBackground(ctx context.Context, tokenName string, cmd commander, ui cli.Ui, flagPort uint) error { - const op = "cache.StartCacheInBackground" + const op = "daemon.StartCacheInBackground" cancelCtx, cancelFunc := context.WithCancel(ctx) diff --git a/internal/cmd/commands/cache/options.go b/internal/cmd/commands/daemon/options.go similarity index 97% rename from internal/cmd/commands/cache/options.go rename to internal/cmd/commands/daemon/options.go index 15074261e2..63a6f57524 100644 --- a/internal/cmd/commands/cache/options.go +++ b/internal/cmd/commands/daemon/options.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package cache +package daemon import ( "context" diff --git a/internal/cmd/commands/cache/search.go b/internal/cmd/commands/daemon/search.go similarity index 91% rename from internal/cmd/commands/cache/search.go rename to internal/cmd/commands/daemon/search.go index 7ec0ef49b2..a81b5363b2 100644 --- a/internal/cmd/commands/cache/search.go +++ b/internal/cmd/commands/daemon/search.go @@ -1,4 +1,7 @@ -package cache +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package daemon import ( "bytes" @@ -28,16 +31,16 @@ type SearchTargetsCommand struct { } func (c *SearchTargetsCommand) Synopsis() string { - return "Start a Boundary cache server" + return "Start a Boundary daemon" } func (c *SearchTargetsCommand) Help() string { helpText := ` -Usage: boundary cache search [options] +Usage: boundary targets search [options] - Search a boundary cache: + Search a boundary target: - $ boundary cache search + $ boundary targets search For a full list of examples, please see the documentation. @@ -80,7 +83,7 @@ func (c *SearchTargetsCommand) AutocompleteFlags() complete.Flags { } func (c *SearchTargetsCommand) Run(args []string) int { - const op = "cache.(SearchTargetsCommand).Run" + const op = "daemon.(SearchTargetsCommand).Run" ctx := c.Context f := c.Flags() if err := f.Parse(args); err != nil { @@ -93,8 +96,13 @@ func (c *SearchTargetsCommand) Run(args []string) int { c.UI.Error(err.Error()) return base.CommandUserError } - + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return base.CommandUserError + } tf := targetFilterBy{ + boundaryAddr: client.Addr(), tokenName: tokenName, flagNameStartsWith: c.flagNameStartsWith, flagQuery: c.flagQuery, @@ -140,6 +148,7 @@ func (c *SearchTargetsCommand) Run(args []string) int { } return base.CommandSuccess } + func (c *SearchTargetsCommand) printListTable(items []*targets.Target) string { if len(items) == 0 { return "No targets found" @@ -204,7 +213,7 @@ func (c *SearchTargetsCommand) printListTable(items []*targets.Target) string { } func SearchClient(ctx context.Context, addr string, flagOutputCurl bool) (*api.Client, error) { - const op = "cache.SearchClient" + const op = "daemon.SearchClient" client, err := api.NewClient(nil) if err != nil { return nil, errors.Wrap(ctx, err, op) @@ -224,10 +233,11 @@ type targetFilterBy struct { flagNameStartsWith string flagQuery string tokenName string + boundaryAddr string } func searchTargets(ctx context.Context, filterBy targetFilterBy, flagPort uint, flagOutputCurl bool) (*api.Response, error) { - const op = "cache.searchTargets" + const op = "daemon.searchTargets" client, err := SearchClient(ctx, fmt.Sprintf("http://localhost:%d", flagPort), flagOutputCurl) if err != nil { return nil, errors.Wrap(ctx, err, op) @@ -237,6 +247,7 @@ func searchTargets(ctx context.Context, filterBy targetFilterBy, flagPort uint, return nil, errors.Wrap(ctx, err, op, errors.WithMsg("new client request error: %s", err.Error())) } req.Header.Add("token_name", filterBy.tokenName) + req.Header.Add("boundary_addr", filterBy.boundaryAddr) q := url.Values{} q.Add("name_starts_with", filterBy.flagNameStartsWith) q.Add("query", filterBy.flagQuery) diff --git a/internal/cmd/commands/cache/server.go b/internal/cmd/commands/daemon/server.go similarity index 89% rename from internal/cmd/commands/cache/server.go rename to internal/cmd/commands/daemon/server.go index 4d98d467cf..50ac1ca4d6 100644 --- a/internal/cmd/commands/cache/server.go +++ b/internal/cmd/commands/daemon/server.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package cache +package daemon import ( "context" @@ -21,9 +21,9 @@ import ( "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/targets" - "github.com/hashicorp/boundary/internal/cache" "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/internal/cmd/base/logging" + "github.com/hashicorp/boundary/internal/daemon/cache" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/observability/event" @@ -91,7 +91,7 @@ func (sc *serverConfig) validate() error { // can be called before eventing is setup func newServer(ctx context.Context, conf serverConfig) (*server, error) { - const op = "cache.(server).newServer" + const op = "daemon.(server).newServer" if err := conf.validate(); err != nil { return nil, errors.Wrap(ctx, err, op) } @@ -107,7 +107,7 @@ func newServer(ctx context.Context, conf serverConfig) (*server, error) { } func (s *server) shutdown() error { - const op = "cache.(server).Shutdown" + const op = "daemon.(server).Shutdown" var shutdownErr error s.shutdownOnce.Do(func() { @@ -141,7 +141,7 @@ func (s *server) shutdown() error { // daemon. The daemon bits are included so it's easy for CLI cmds to start the // a cache server func (s *server) start(ctx context.Context, port uint) error { - const op = "cache.(server).start" + const op = "daemon.(server).start" switch { case util.IsNil(ctx): return errors.New(ctx, errors.InvalidParameter, op, "context is missing") @@ -174,11 +174,11 @@ func (s *server) start(ctx context.Context, port uint) error { } daemonCtx = &daemon.Context{ PidFileName: fmt.Sprintf(pidFileNameTemplate, homeDir), - PidFilePerm: 0644, + PidFilePerm: 0o644, LogFileName: fmt.Sprintf(logFileNameTemplate, homeDir), - LogFilePerm: 0640, + LogFilePerm: 0o640, WorkDir: homeDir, - Umask: 027, + Umask: 0o27, } termHandler := func(sig os.Signal) error { @@ -319,7 +319,7 @@ const ( ) func newSearchTargetsHandlerFunc(ctx context.Context, store *cache.Store, tokenName string) (http.HandlerFunc, error) { - const op = "cache.newSearchTargetsHandlerFunc" + const op = "daemon.newSearchTargetsHandlerFunc" switch { case util.IsNil(store): return nil, errors.New(ctx, errors.InvalidParameter, op, "store is missing") @@ -339,6 +339,13 @@ func newSearchTargetsHandlerFunc(ctx context.Context, store *cache.Store, tokenN http.Error(w, "Forbidden", http.StatusForbidden) return } + boundaryAddr := r.Header.Get("boundary_addr") + + // TODO: Look up the persona from fields passed in. For now just hard code the addr and token. + p := &cache.Persona{ + BoundaryAddr: boundaryAddr, + TokenName: reqTokenName, + } repo, err := cache.NewRepository(ctx, store) if err != nil { @@ -349,25 +356,25 @@ func newSearchTargetsHandlerFunc(ctx context.Context, store *cache.Store, tokenN var found []*targets.Target switch { case r.URL.Query().Get(queryKey) != "": - found, err = repo.QueryTargets(r.Context(), tokenName, r.URL.Query().Get(queryKey)) + found, err = repo.QueryTargets(r.Context(), p, r.URL.Query().Get(queryKey)) default: found, err = repo.FindTargets( r.Context(), - tokenName, - cache.WithIdContains(ctx, r.URL.Query().Get(idContainsKey)), - cache.WithNameContains(ctx, r.URL.Query().Get(nameContainsKey)), - cache.WithDescriptionContains(ctx, r.URL.Query().Get(descriptionContainsKey)), - cache.WithAddressContains(ctx, r.URL.Query().Get(addressContainsKey)), - - cache.WithIdStartsWith(ctx, r.URL.Query().Get(idStartsWithKey)), - cache.WithNameStartsWith(ctx, r.URL.Query().Get(nameStartsWithKey)), - cache.WithDescriptionStartsWith(ctx, r.URL.Query().Get(descriptionStartsWithKey)), - cache.WithAddressStartsWith(ctx, r.URL.Query().Get(addressStartsWithKey)), - - cache.WithIdEndsWith(ctx, r.URL.Query().Get(idEndsWithKey)), - cache.WithNameEndsWith(ctx, r.URL.Query().Get(nameEndsWithKey)), - cache.WithDescriptionEndsWith(ctx, r.URL.Query().Get(descriptionEndsWithKey)), - cache.WithAddressEndsWith(ctx, r.URL.Query().Get(addressEndsWithKey)), + p, + cache.WithIdContains(r.URL.Query().Get(idContainsKey)), + cache.WithNameContains(r.URL.Query().Get(nameContainsKey)), + cache.WithDescriptionContains(r.URL.Query().Get(descriptionContainsKey)), + cache.WithAddressContains(r.URL.Query().Get(addressContainsKey)), + + cache.WithIdStartsWith(r.URL.Query().Get(idStartsWithKey)), + cache.WithNameStartsWith(r.URL.Query().Get(nameStartsWithKey)), + cache.WithDescriptionStartsWith(r.URL.Query().Get(descriptionStartsWithKey)), + cache.WithAddressStartsWith(r.URL.Query().Get(addressStartsWithKey)), + + cache.WithIdEndsWith(r.URL.Query().Get(idEndsWithKey)), + cache.WithNameEndsWith(r.URL.Query().Get(nameEndsWithKey)), + cache.WithDescriptionEndsWith(r.URL.Query().Get(descriptionEndsWithKey)), + cache.WithAddressEndsWith(r.URL.Query().Get(addressEndsWithKey)), ) } if err != nil { @@ -394,7 +401,6 @@ func newSearchTargetsHandlerFunc(ctx context.Context, store *cache.Store, tokenN return } w.Write(j) - }, nil } @@ -438,7 +444,7 @@ func (s *server) printInfo(ui cli.Ui) { } func (s *server) setupLogging(flagLogLevel, flagLogFormat string) (logging.LogFormat, hclog.Level, error) { - const op = "cache.(Command).setupLogging" + const op = "daemon.(Command).setupLogging" // flagLogLevel and flagLogFormat are still valid when empty s.logOutput = os.Stderr @@ -491,7 +497,7 @@ func (s *server) setupLogging(flagLogLevel, flagLogFormat string) (logging.LogFo } func setupEventing(ctx context.Context, logger hclog.Logger, serializationLock *sync.Mutex, logFormat logging.LogFormat) (*event.Eventer, error) { - const op = "cache.setupEventing" + const op = "daemon.setupEventing" switch { case util.IsNil(logger): return nil, errors.New(ctx, errors.InvalidParameter, op, "logger is missing") @@ -541,7 +547,7 @@ func setupEventing(ctx context.Context, logger hclog.Logger, serializationLock * } func openStore(ctx context.Context, url string, flagDebugStore bool) (*cache.Store, string, error) { - const op = "cache.openStore" + const op = "daemon.openStore" var err error switch { case url != "": @@ -552,7 +558,7 @@ func openStore(ctx context.Context, url string, flagDebugStore bool) (*cache.Sto default: url = cache.DefaultStoreUrl } - store, err := cache.Open(ctx, cache.WithUrl(ctx, url), cache.WithDebug(ctx, flagDebugStore)) + store, err := cache.Open(ctx, cache.WithUrl(url), cache.WithDebug(flagDebugStore)) if err != nil { return nil, "", errors.Wrap(ctx, err, op) } diff --git a/internal/cmd/commands/cache/ticker.go b/internal/cmd/commands/daemon/ticker.go similarity index 74% rename from internal/cmd/commands/cache/ticker.go rename to internal/cmd/commands/daemon/ticker.go index 012b38a793..83b838d966 100644 --- a/internal/cmd/commands/cache/ticker.go +++ b/internal/cmd/commands/daemon/ticker.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package cache +package daemon import ( "context" @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/targets" - "github.com/hashicorp/boundary/internal/cache" + "github.com/hashicorp/boundary/internal/daemon/cache" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/boundary/internal/types/resource" @@ -26,7 +26,7 @@ type refreshTicker struct { } func newRefreshTicker(ctx context.Context, refreshIntervalSeconds int64, cmd commander, store *cache.Store, tokenName string) (*refreshTicker, error) { - const op = "cache.newRefreshTicker" + const op = "daemon.newRefreshTicker" switch { case refreshIntervalSeconds == 0: return nil, errors.New(ctx, errors.InvalidParameter, op, "refresh interval seconds is missing") @@ -57,7 +57,7 @@ func newRefreshTicker(ctx context.Context, refreshIntervalSeconds int64, cmd com } func (rt *refreshTicker) start(ctx context.Context) { - const op = "cache.(refreshTicker).start" + const op = "daemon.(refreshTicker).start" timer := time.NewTimer(0) for { select { @@ -71,13 +71,21 @@ func (rt *refreshTicker) start(ctx context.Context) { timer.Reset(rt.refreshInterval) continue } + if client.Addr() == "" { + // emit event, reset, and continue + errors.New(ctx, errors.InvalidParameter, op, "boundary address is missing") + timer.Reset(rt.refreshInterval) + continue + } if client.Token() == "" { // emit event, reset, and continue errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("token name %q is missing", rt.tokenName)) timer.Reset(rt.refreshInterval) continue } - if err := refreshCache(ctx, client, rt.tokenName, rt.store); err != nil { + // TODO: Iterate over personas and use their address and token information instead of what + // was available at the time the ticker was started. + if err := refreshCache(ctx, client, client.Addr(), rt.tokenName, rt.store); err != nil { // event was already emitted, so reset and continue timer.Reset(rt.refreshInterval) continue @@ -87,13 +95,15 @@ func (rt *refreshTicker) start(ctx context.Context) { } } -func refreshCache(ctx context.Context, client *api.Client, tokenName string, store *cache.Store) error { - const op = "cache.(Repository).refreshCache" +func refreshCache(ctx context.Context, client *api.Client, addr string, tokenName string, store *cache.Store) error { + const op = "daemon.(Repository).refreshCache" switch { case util.IsNil(client): return errors.New(ctx, errors.InvalidParameter, op, "api client is missing") case tokenName == "": return errors.New(ctx, errors.InvalidParameter, op, "token name is missing") + case addr == "": + return errors.New(ctx, errors.InvalidParameter, op, "boundary address is missing") case util.IsNil(store): return errors.New(ctx, errors.InvalidParameter, op, "store is missing") } @@ -112,7 +122,15 @@ func refreshCache(ctx context.Context, client *api.Client, tokenName string, sto } event.WriteSysEvent(ctx, op, fmt.Sprintf("updating %d targets", len(l.Items))) - if err := r.RefreshTargets(ctx, tokenName, l.Items); err != nil { + + p := &cache.Persona{ + BoundaryAddr: addr, + TokenName: tokenName, + } + if err := r.AddPersona(ctx, p); err != nil { + return errors.Wrap(ctx, err, op) + } + if err := r.RefreshTargets(ctx, p, l.Items); err != nil { return errors.Wrap(ctx, err, op) } return nil diff --git a/internal/cmd/main.go b/internal/cmd/main.go index 54e3fca9b5..382437178f 100644 --- a/internal/cmd/main.go +++ b/internal/cmd/main.go @@ -18,7 +18,7 @@ import ( "github.com/fatih/color" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/internal/cmd/base" - "github.com/hashicorp/boundary/internal/cmd/commands/cache" + "github.com/hashicorp/boundary/internal/cmd/commands/daemon" colorable "github.com/mattn/go-colorable" "github.com/mitchellh/cli" ) @@ -220,14 +220,14 @@ func RunCustom(args []string, runOpts *RunOptions) int { l = append(l, strings.ToLower(arg)) } currentCmd := strings.Join(l, "-") - if currentCmd != "" && !strings.HasPrefix(currentCmd, "cache-server") { - c, err := cli.Commands["cache server"]() + if currentCmd != "" && !strings.HasPrefix(currentCmd, "daemon") { + c, err := cli.Commands["daemon start"]() if err != nil { fmt.Fprintf(runOpts.Stderr, "Error creating command: %s\n", err.Error()) return } - serverCmd, ok := c.(*cache.ServerCommand) + serverCmd, ok := c.(*daemon.ServerCommand) if !ok { fmt.Fprintf(runOpts.Stderr, "Error base command: %s\n", err.Error()) return @@ -240,7 +240,7 @@ func RunCustom(args []string, runOpts *RunOptions) int { return } - err = cache.StartCacheInBackground(context.Background(), tokenName, serverCmd, serverCmd.UI, 9203) + err = daemon.StartCacheInBackground(context.Background(), tokenName, serverCmd, serverCmd.UI, 9203) if err != nil && !strings.Contains(err.Error(), "already running") { return } diff --git a/internal/cache/options.go b/internal/daemon/cache/options.go similarity index 76% rename from internal/cache/options.go rename to internal/daemon/cache/options.go index a198b0bac2..749ab07ff8 100644 --- a/internal/cache/options.go +++ b/internal/daemon/cache/options.go @@ -4,8 +4,6 @@ package cache import ( - "context" - "github.com/hashicorp/go-dbw" ) @@ -48,7 +46,7 @@ func getOpts(opt ...Option) (options, error) { } // WithUrls provides optional url -func WithUrl(ctx context.Context, url string) Option { +func WithUrl(url string) Option { const op = "cache.WithUrl" return func(o *options) error { o.withUrl = url @@ -57,7 +55,7 @@ func WithUrl(ctx context.Context, url string) Option { } // WithNameContains provides an optional name contains value. -func WithNameContains(_ context.Context, value string) Option { +func WithNameContains(value string) Option { return func(o *options) error { o.withNameContains = value return nil @@ -65,7 +63,7 @@ func WithNameContains(_ context.Context, value string) Option { } // WithNameStartsWith provides an optional name starts with value. -func WithNameStartsWith(_ context.Context, value string) Option { +func WithNameStartsWith(value string) Option { return func(o *options) error { o.withNameStartsWith = value return nil @@ -73,7 +71,7 @@ func WithNameStartsWith(_ context.Context, value string) Option { } // WithNameEndsWith provides an optional name ends with value. -func WithNameEndsWith(_ context.Context, value string) Option { +func WithNameEndsWith(value string) Option { return func(o *options) error { o.withNameEndsWith = value return nil @@ -81,7 +79,7 @@ func WithNameEndsWith(_ context.Context, value string) Option { } // WithDescriptionContains provides an optional description contains value. -func WithDescriptionContains(_ context.Context, value string) Option { +func WithDescriptionContains(value string) Option { return func(o *options) error { o.withDescriptionContains = value return nil @@ -89,7 +87,7 @@ func WithDescriptionContains(_ context.Context, value string) Option { } // WithDescriptionStartsWith provides an optional description starts with value. -func WithDescriptionStartsWith(_ context.Context, value string) Option { +func WithDescriptionStartsWith(value string) Option { return func(o *options) error { o.withDescriptionStartsWith = value return nil @@ -97,7 +95,7 @@ func WithDescriptionStartsWith(_ context.Context, value string) Option { } // WithDescriptionEndsWith provides an optional description ends with value. -func WithDescriptionEndsWith(_ context.Context, value string) Option { +func WithDescriptionEndsWith(value string) Option { return func(o *options) error { o.withDescriptionEndsWith = value return nil @@ -105,7 +103,7 @@ func WithDescriptionEndsWith(_ context.Context, value string) Option { } // WithIdContains provides an optional id contains value. -func WithIdContains(_ context.Context, value string) Option { +func WithIdContains(value string) Option { return func(o *options) error { o.withIdContains = value return nil @@ -113,7 +111,7 @@ func WithIdContains(_ context.Context, value string) Option { } // WithIdStartsWith provides an optional id starts with value. -func WithIdStartsWith(_ context.Context, value string) Option { +func WithIdStartsWith(value string) Option { return func(o *options) error { o.withIdStartsWith = value return nil @@ -121,7 +119,7 @@ func WithIdStartsWith(_ context.Context, value string) Option { } // WithIdEndsWith provides an optional id ends with value. -func WithIdEndsWith(_ context.Context, value string) Option { +func WithIdEndsWith(value string) Option { return func(o *options) error { o.withIdEndsWith = value return nil @@ -129,7 +127,7 @@ func WithIdEndsWith(_ context.Context, value string) Option { } // WithAddressContains provides an optional address contains value. -func WithAddressContains(_ context.Context, value string) Option { +func WithAddressContains(value string) Option { return func(o *options) error { o.withAddressContains = value return nil @@ -137,7 +135,7 @@ func WithAddressContains(_ context.Context, value string) Option { } // WithAddressStartsWith provides an optional address starts with value. -func WithAddressStartsWith(_ context.Context, value string) Option { +func WithAddressStartsWith(value string) Option { return func(o *options) error { o.withAddressStartsWith = value return nil @@ -145,7 +143,7 @@ func WithAddressStartsWith(_ context.Context, value string) Option { } // WithAddressEndsWith provides an optional address ends with value. -func WithAddressEndsWith(_ context.Context, value string) Option { +func WithAddressEndsWith(value string) Option { return func(o *options) error { o.withAddressEndsWith = value return nil @@ -153,7 +151,7 @@ func WithAddressEndsWith(_ context.Context, value string) Option { } // WithDbType provides an optional db type. -func WithDbType(_ context.Context, dbType dbw.DbType) Option { +func WithDbType(dbType dbw.DbType) Option { return func(o *options) error { o.withDbType = dbType return nil @@ -161,7 +159,7 @@ func WithDbType(_ context.Context, dbType dbw.DbType) Option { } // WithDebug provides an optional debug flag. -func WithDebug(_ context.Context, debug bool) Option { +func WithDebug(debug bool) Option { return func(o *options) error { o.withDebug = debug return nil diff --git a/internal/cache/query.go b/internal/daemon/cache/query.go similarity index 100% rename from internal/cache/query.go rename to internal/daemon/cache/query.go diff --git a/internal/cache/repository.go b/internal/daemon/cache/repository.go similarity index 52% rename from internal/cache/repository.go rename to internal/daemon/cache/repository.go index b0508bb334..de1d914bd5 100644 --- a/internal/cache/repository.go +++ b/internal/daemon/cache/repository.go @@ -21,8 +21,10 @@ import ( "github.com/seldonio/goven/sql_adaptor" ) +const personaLimit = 50 + type Repository struct { - s *Store + rw *db.Db } func NewRepository(ctx context.Context, s *Store) (*Repository, error) { @@ -31,12 +33,99 @@ func NewRepository(ctx context.Context, s *Store) (*Repository, error) { case util.IsNil(s): return nil, errors.New(ctx, errors.InvalidParameter, op, "missing store") } - return &Repository{s: s}, nil + return &Repository{rw: db.New(s.conn)}, nil +} + +// AddPersona adds a persona to the repository. If the number of personas now +// exceed a limit, the persona retrieved least recently is deleted. +func (r *Repository) AddPersona(ctx context.Context, p *Persona) error { + const op = "cache.(Repository).AddPersona" + switch { + case p == nil: + return errors.New(ctx, errors.InvalidParameter, op, "persona is nil") + case p.TokenName == "": + return errors.New(ctx, errors.InvalidParameter, op, "persona's token name is empty") + case p.BoundaryAddr == "": + return errors.New(ctx, errors.InvalidParameter, op, "persona's boundary address is empty") + } + + onConflict := db.OnConflict{ + Target: db.Columns{"boundary_addr", "token_name"}, + Action: db.SetColumns([]string{"auth_token_id", "last_accessed_time"}), + } + _, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, writer db.Writer) error { + if err := writer.Create(ctx, p, db.WithOnConflict(&onConflict)); err != nil { + return errors.Wrap(ctx, err, op) + } + + var personas []*Persona + if err := reader.SearchWhere(ctx, &personas, "", []any{}, db.WithLimit(-1)); err != nil { + return errors.Wrap(ctx, err, op) + } + if len(personas) <= personaLimit { + return nil + } + + var oldestPersona *Persona + for _, p := range personas { + if oldestPersona == nil || oldestPersona.LastAccessedTime.After(p.LastAccessedTime) { + oldestPersona = p + } + } + if oldestPersona != nil { + if _, err := writer.Delete(ctx, oldestPersona); err != nil { + return errors.Wrap(ctx, err, op) + } + } + return nil + }) + if err != nil { + return err + } + + return nil +} + +// LookupPersona returns the persona and updates its last accessed time +func (r *Repository) LookupPersona(ctx context.Context, addr string, tokenName string) (*Persona, error) { + const op = "cache.(Repository).LookupPersona" + switch { + case addr == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "address is empty") + case tokenName == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "token name is empty") + } + + p := &Persona{ + BoundaryAddr: addr, + TokenName: tokenName, + } + if err := r.rw.LookupById(ctx, p); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op) + } + updatedP := &Persona{ + BoundaryAddr: p.BoundaryAddr, + TokenName: p.TokenName, + LastAccessedTime: time.Now(), + } + if _, err := r.rw.Update(ctx, updatedP, []string{"LastAccessedTime"}, nil); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + return updatedP, nil } func (r *Repository) SaveError(ctx context.Context, resourceType string, err error) error { const op = "cache.(Repository).StoreError" - rw := db.New(r.s.conn) + switch { + case resourceType == "": + return errors.New(ctx, errors.InvalidParameter, op, "resource type is empty") + case err == nil: + return errors.New(ctx, errors.InvalidParameter, op, "error is nil") + } apiErr := &ApiError{ ResourceType: resourceType, Error: err.Error(), @@ -45,37 +134,49 @@ func (r *Repository) SaveError(ctx context.Context, resourceType string, err err Target: db.Columns{"token_name", "resource_type"}, Action: db.SetColumns([]string{"error", "create_time"}), } - if err := rw.Create(ctx, apiErr, db.WithOnConflict(&onConflict)); err != nil { + if err := r.rw.Create(ctx, apiErr, db.WithOnConflict(&onConflict)); err != nil { return errors.Wrap(ctx, err, op) } return nil } -func (r *Repository) RefreshTargets(ctx context.Context, tokenName string, targets []*targets.Target) error { +func (r *Repository) RefreshTargets(ctx context.Context, p *Persona, targets []*targets.Target) error { const op = "cache.(Repository).RefreshTargets" - rw := db.New(r.s.conn) - _, err := rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error { - if _, err := rw.Exec(ctx, "delete from cache_target where token_name = @token_name", []any{sql.Named("token_name", tokenName)}); err != nil { + switch { + case p == nil: + return errors.New(ctx, errors.InvalidParameter, op, "persona is nil") + case p.TokenName == "": + return errors.New(ctx, errors.InvalidParameter, op, "token name is missing") + case p.BoundaryAddr == "": + return errors.New(ctx, errors.InvalidParameter, op, "boundary address is missing") + } + + _, err := r.rw.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, w db.Writer) error { + // TODO: Instead of deleting everything, use refresh tokens and apply the delta + if _, err := w.Exec(ctx, "delete from cache_target where boundary_addr = @boundary_addr and token_name = @token_name", + []any{sql.Named("boundary_addr", p.BoundaryAddr), sql.Named("token_name", p.TokenName)}); err != nil { return err } + for _, t := range targets { item, err := json.Marshal(t) if err != nil { return err } newTarget := Target{ - TokenName: tokenName, - Id: t.Id, - Name: t.Name, - Description: t.Description, - Address: t.Address, - Item: string(item), + BoundaryAddr: p.BoundaryAddr, + TokenName: p.TokenName, + Id: t.Id, + Name: t.Name, + Description: t.Description, + Address: t.Address, + Item: string(item), } onConflict := db.OnConflict{ - Target: db.Columns{"token_name", "id"}, - Action: db.SetColumns([]string{"name", "description", "address", "item"}), + Target: db.Columns{"boundary_addr", "token_name", "id"}, + Action: db.UpdateAll(true), } - if err := rw.Create(ctx, newTarget, db.WithOnConflict(&onConflict)); err != nil { + if err := w.Create(ctx, newTarget, db.WithOnConflict(&onConflict)); err != nil { return err } } @@ -90,15 +191,18 @@ func (r *Repository) RefreshTargets(ctx context.Context, tokenName string, targe return nil } -func (r *Repository) QueryTargets(ctx context.Context, tokenName string, query string) ([]*targets.Target, error) { +func (r *Repository) QueryTargets(ctx context.Context, p *Persona, query string) ([]*targets.Target, error) { const op = "cache.(Repository).QueryTargets" switch { - case tokenName == "": + case p == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "persona is missing") + case p.TokenName == "": return nil, errors.New(ctx, errors.InvalidParameter, op, "token name is missing") + case p.BoundaryAddr == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "boundary address is missing") case query == "": return nil, errors.New(ctx, errors.InvalidParameter, op, "query is missing") } - rw := db.New(r.s.conn) var cachedTargets []*Target reflection := reflect.ValueOf(&Target{}) @@ -115,7 +219,7 @@ func (r *Repository) QueryTargets(ctx context.Context, tokenName string, query s if err != nil { return nil, errors.Wrap(ctx, err, op) } - if err := rw.SearchWhere(ctx, &cachedTargets, parsedQuery.Raw, sql_adaptor.StringSliceToInterfaceSlice(parsedQuery.Values), db.WithLimit(-1)); err != nil { + if err := r.rw.SearchWhere(ctx, &cachedTargets, parsedQuery.Raw, sql_adaptor.StringSliceToInterfaceSlice(parsedQuery.Values), db.WithLimit(-1)); err != nil { return nil, errors.Wrap(ctx, err, op) } retTargets := make([]*targets.Target, 0, len(cachedTargets)) @@ -129,21 +233,24 @@ func (r *Repository) QueryTargets(ctx context.Context, tokenName string, query s return retTargets, nil } -func (r *Repository) FindTargets(ctx context.Context, tokenName string, opt ...Option) ([]*targets.Target, error) { +func (r *Repository) FindTargets(ctx context.Context, p *Persona, opt ...Option) ([]*targets.Target, error) { const op = "cache.(Repository).FindTargets" switch { - case tokenName == "": + case p == nil: + return nil, errors.New(ctx, errors.InvalidParameter, op, "persona is missing") + case p.TokenName == "": return nil, errors.New(ctx, errors.InvalidParameter, op, "token name is missing") + case p.BoundaryAddr == "": + return nil, errors.New(ctx, errors.InvalidParameter, op, "boundary address is missing") } - rw := db.New(r.s.conn) var cachedTargets []*Target opts, err := getOpts(opt...) if err != nil { return nil, errors.Wrap(ctx, err, op) } - whereClause := []string{"token_name = @token_name"} - whereParameters := []any{sql.Named("token_name", tokenName)} + whereClause := []string{"boundary_addr = @boundary_addr and token_name = @token_name"} + whereParameters := []any{sql.Named("boundary_addr", p.BoundaryAddr), sql.Named("token_name", p.TokenName)} if opts.withIdContains != "" { whereClause = append(whereClause, "id like @contains_id") @@ -196,7 +303,7 @@ func (r *Repository) FindTargets(ctx context.Context, tokenName string, opt ...O whereParameters = append(whereParameters, sql.Named("ends_with_address", "%"+opts.withAddressEndsWith)) } - if err := rw.SearchWhere(ctx, &cachedTargets, strings.Join(whereClause, " and "), whereParameters, db.WithLimit(-1)); err != nil { + if err := r.rw.SearchWhere(ctx, &cachedTargets, strings.Join(whereClause, " and "), whereParameters, db.WithLimit(-1)); err != nil { return nil, errors.Wrap(ctx, err, op) } retTargets := make([]*targets.Target, 0, len(cachedTargets)) @@ -211,13 +318,13 @@ func (r *Repository) FindTargets(ctx context.Context, tokenName string, opt ...O } type Target struct { - TokenName string `gorm:"primaryKey"` - Id string `gorm:"primaryKey"` - Name string - Description string - Address string - Item string - CreatedTime time.Time + BoundaryAddr string `gorm:"primaryKey"` + TokenName string `gorm:"primaryKey"` + Id string `gorm:"primaryKey"` + Name string + Description string + Address string + Item string } func (*Target) TableName() string { @@ -234,3 +341,14 @@ type ApiError struct { func (*ApiError) TableName() string { return "cache_api_error" } + +type Persona struct { + BoundaryAddr string `gorm:"primaryKey"` + TokenName string `gorm:"primaryKey"` + AuthTokenId string + LastAccessedTime time.Time `gorm:"default:(strftime('%Y-%m-%d %H:%M:%f','now'))"` +} + +func (*Persona) TableName() string { + return "cache_persona" +} diff --git a/internal/daemon/cache/repository_test.go b/internal/daemon/cache/repository_test.go new file mode 100644 index 0000000000..d1348db13f --- /dev/null +++ b/internal/daemon/cache/repository_test.go @@ -0,0 +1,209 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cache + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/api/targets" + "github.com/hashicorp/boundary/internal/db" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_AddPersona(t *testing.T) { + ctx := context.Background() + s, err := Open(ctx) + require.NoError(t, err) + + r, err := NewRepository(ctx, s) + require.NoError(t, err) + + addr := "address" + p := &Persona{ + BoundaryAddr: addr, + TokenName: "default", + AuthTokenId: "at_1234567890", + } + assert.NoError(t, r.AddPersona(ctx, p)) + assert.NoError(t, r.AddPersona(ctx, p)) + for i := 0; i < personaLimit; i++ { + p.BoundaryAddr = fmt.Sprintf("%s%d", addr, i) + assert.NoError(t, r.AddPersona(ctx, p)) + } + // Lookup the first persona added. It should have been evicted for being + // used the least recently. + gotP, err := r.LookupPersona(ctx, addr, p.TokenName) + assert.NoError(t, err) + assert.Nil(t, gotP) + + p, err = r.LookupPersona(ctx, addr+"0", p.TokenName) + assert.NoError(t, err) + assert.NotNil(t, p) + t.Logf("got %#v", p) +} + +func TestRepository_LookupPersona(t *testing.T) { + ctx := context.Background() + s, err := Open(ctx) + require.NoError(t, err) + + r, err := NewRepository(ctx, s) + require.NoError(t, err) + + t.Run("empty address", func(t *testing.T) { + p, err := r.LookupPersona(ctx, "", "token") + assert.ErrorContains(t, err, "address is empty") + assert.Nil(t, p) + }) + t.Run("empty token name", func(t *testing.T) { + p, err := r.LookupPersona(ctx, "address", "") + assert.ErrorContains(t, err, "token name is empty") + assert.Nil(t, p) + }) + t.Run("not found", func(t *testing.T) { + p, err := r.LookupPersona(ctx, "address", "token") + assert.NoError(t, err) + assert.Nil(t, p) + }) + t.Run("found", func(t *testing.T) { + addr := "address" + p := &Persona{ + BoundaryAddr: addr, + TokenName: "default", + AuthTokenId: "at_1234567890", + } + assert.NoError(t, r.AddPersona(ctx, p)) + p, err := r.LookupPersona(ctx, p.BoundaryAddr, p.TokenName) + assert.NoError(t, err) + assert.NotNil(t, p) + }) +} + +func TestRepository_RefreshTargets(t *testing.T) { + ctx := context.Background() + s, err := Open(ctx) + require.NoError(t, err) + + r, err := NewRepository(ctx, s) + require.NoError(t, err) + + addr := "address" + p := &Persona{ + BoundaryAddr: addr, + TokenName: "default", + AuthTokenId: "at_1234567890", + } + require.NoError(t, r.AddPersona(ctx, p)) + + ts := []*targets.Target{ + { + Id: "ttcp_1", + Name: "name1", + Address: "address1", + Type: "tcp", + SessionMaxSeconds: 111, + }, + { + Id: "ttcp_2", + Name: "name2", + Address: "address2", + Type: "tcp", + SessionMaxSeconds: 222, + }, + { + Id: "ttcp_3", + Name: "name3", + Address: "address3", + Type: "tcp", + SessionMaxSeconds: 333, + }, + } + cases := []struct { + name string + persona *Persona + targets []*targets.Target + wantCount int + errorContains string + }{ + { + name: "Success", + persona: p, + targets: ts, + wantCount: len(ts), + }, + { + name: "repeated target with different values", + persona: p, + targets: append(ts, &targets.Target{ + Id: ts[0].Id, + Name: "a different name", + }), + wantCount: len(ts), + }, + { + name: "nil persona", + persona: nil, + targets: ts, + errorContains: "persona is nil", + }, + { + name: "missing token name", + persona: &Persona{ + BoundaryAddr: p.BoundaryAddr, + }, + targets: ts, + errorContains: "token name is missing", + }, + { + name: "missing boundary address", + persona: &Persona{ + TokenName: p.TokenName, + }, + targets: ts, + errorContains: "boundary address is missing", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := r.RefreshTargets(ctx, tc.persona, tc.targets) + if tc.errorContains == "" { + assert.NoError(t, err) + rw := db.New(s.conn) + var got []*Target + require.NoError(t, rw.SearchWhere(ctx, &got, "true", nil)) + assert.Len(t, got, tc.wantCount) + } else { + assert.ErrorContains(t, err, tc.errorContains) + } + }) + } +} + +func TestRepository_SaveError(t *testing.T) { + ctx := context.Background() + s, err := Open(ctx, WithDebug(true)) + require.NoError(t, err) + + r, err := NewRepository(ctx, s) + require.NoError(t, err) + + testResource := "test_resource_type" + testErr := fmt.Errorf("test error for %q", testResource) + + t.Run("empty resource type", func(t *testing.T) { + assert.ErrorContains(t, r.SaveError(ctx, "", testErr), "resource type is empty") + }) + t.Run("nil error", func(t *testing.T) { + assert.ErrorContains(t, r.SaveError(ctx, testResource, nil), "error is nil") + }) + t.Run("success", func(t *testing.T) { + assert.NoError(t, r.SaveError(ctx, testResource, testErr)) + }) + + assert.NoError(t, r.SaveError(ctx, testResource, testErr)) +} diff --git a/internal/cache/store.go b/internal/daemon/cache/store.go similarity index 69% rename from internal/cache/store.go rename to internal/daemon/cache/store.go index 479319f24c..25430a4e3c 100644 --- a/internal/cache/store.go +++ b/internal/daemon/cache/store.go @@ -13,8 +13,8 @@ import ( "github.com/hashicorp/go-dbw" ) -// DefaultStoreUrl uses a temp in-memory sqlite database (shared) see: https://www.sqlite.org/inmemorydb.html -const DefaultStoreUrl = "file::memory:?cache=shared" +// DefaultStoreUrl uses a temp in-memory sqlite database see: https://www.sqlite.org/inmemorydb.html +const DefaultStoreUrl = "file::memory:?_pragma=foreign_keys(1)" type Store struct { conn *db.DB @@ -64,15 +64,27 @@ const ( createTables = ` begin; +create table if not exists cache_persona ( + boundary_addr text not null, + token_name text not null, + auth_token_id text not null, -- Expected token id stored in the token name + -- the timestamp has this default in order for the db to store fractional seconds. + last_accessed_time timestamp not null default (strftime('%Y-%m-%d %H:%M:%f','now')), + primary key (boundary_addr, token_name) +); + create table if not exists cache_target ( + boundary_addr text not null, token_name text not null, id text not null, name text, description text, address text, item text, - created_time timestamp default current_timestamp, - primary key (token_name, id) + foreign key (boundary_addr, token_name) + references cache_persona(boundary_addr, token_name) + on delete cascade, + primary key (boundary_addr, token_name, id) ); create table if not exists cache_api_error ( diff --git a/internal/daemon/cache/store_test.go b/internal/daemon/cache/store_test.go new file mode 100644 index 0000000000..9a1ebdc058 --- /dev/null +++ b/internal/daemon/cache/store_test.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cache + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPersona(t *testing.T) { + ctx := context.Background() + s, err := Open(ctx, WithDebug(true)) + require.NoError(t, err) + + rw := db.New(s.conn) + + p := Persona{ + BoundaryAddr: "boundary", + TokenName: "default", + AuthTokenId: "at_1234567890", + } + before := time.Now().Truncate(1 * time.Millisecond) + require.NoError(t, rw.Create(ctx, &p)) + + require.NoError(t, rw.LookupById(ctx, &p)) + assert.GreaterOrEqual(t, p.LastAccessedTime, before) + + p.AuthTokenId = "at_0987654321" + n, err := rw.Update(ctx, &p, []string{"AuthTokenId"}, nil) + assert.NoError(t, err) + assert.Equal(t, 1, n) + + n, err = rw.Delete(ctx, &p) + assert.NoError(t, err) + assert.Equal(t, 1, n) +} + +func TestTarget(t *testing.T) { + ctx := context.Background() + s, err := Open(ctx, WithDebug(true)) + require.NoError(t, err) + + rw := db.New(s.conn) + + p := &Persona{ + BoundaryAddr: "boundary", + TokenName: "default", + AuthTokenId: "at_1234567890", + } + require.NoError(t, rw.Create(ctx, p)) + + t.Run("target without persona", func(t *testing.T) { + unknownTarget := &Target{ + BoundaryAddr: "some unknown addr", + TokenName: "some token name", + Id: "tssh_1234567890", + Name: "target", + Description: "target desc", + Address: "some address", + Item: "{id:'tssh_1234567890'}", + } + require.ErrorContains(t, rw.Create(ctx, unknownTarget), "FOREIGN KEY constraint") + }) + + t.Run("target actions", func(t *testing.T) { + target := &Target{ + BoundaryAddr: p.BoundaryAddr, + TokenName: p.TokenName, + Id: "tssh_1234567890", + Name: "target", + Description: "target desc", + Address: "some address", + Item: "{id:'tssh_1234567890'}", + } + + require.NoError(t, rw.Create(ctx, target)) + + require.NoError(t, rw.LookupById(ctx, target)) + + target.Address = "new address" + n, err := rw.Update(ctx, target, []string{"address"}, nil) + assert.NoError(t, err) + assert.Equal(t, 1, n) + + n, err = rw.Delete(ctx, target) + assert.NoError(t, err) + assert.Equal(t, 1, n) + }) + + target := &Target{ + BoundaryAddr: p.BoundaryAddr, + TokenName: p.TokenName, + Id: "tssh_1234567890", + Name: "target", + Description: "target desc", + Address: "some address", + Item: "{id:'tssh_1234567890'}", + } + require.NoError(t, rw.Create(ctx, target)) + + t.Run("lookup a target", func(t *testing.T) { + lookTar := &Target{ + BoundaryAddr: target.BoundaryAddr, + TokenName: target.TokenName, + Id: target.Id, + } + assert.NoError(t, rw.LookupById(ctx, lookTar)) + assert.NotNil(t, lookTar) + }) + + t.Run("deleting the persona deletes the target", func(t *testing.T) { + n, err := rw.Delete(ctx, p) + require.NoError(t, err) + require.Equal(t, 1, n) + + lookTar := &Target{ + BoundaryAddr: target.BoundaryAddr, + TokenName: target.TokenName, + Id: target.Id, + } + assert.ErrorContains(t, rw.LookupById(ctx, lookTar), "not found") + }) +}