Add status command to the boundary client cache (#3871)
* Add status command to the boundary client cache
This commit is contained in:
1 parent
3a7ea64a31
commit
71b09222ae
17 files changed
+995
-23
No files matched your search
@@ -0,0 +1,211 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderr "errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/daemon"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/version"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
var errDaemonNotRunning = stderr.New("daemon 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 daemon"
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: boundary daemon status [options]
|
||||
|
||||
Get the status of the boundary daemon:
|
||||
|
||||
$ boundary daemon 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.FlagSetClient | base.FlagSetOutputFormat)
|
||||
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, err := c.Status(ctx)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
res := &daemon.StatusResult{}
|
||||
apiErr, err := resp.Decode(res)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
if apiErr != nil {
|
||||
c.PrintApiError(apiErr, "Error from daemon when getting the status")
|
||||
return base.CommandApiError
|
||||
|
||||
}
|
||||
switch base.Format(c.UI) {
|
||||
case "json":
|
||||
c.UI.Output(resp.Body.String())
|
||||
default:
|
||||
c.UI.Output(printStatusTable(res))
|
||||
}
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Status(ctx context.Context) (*api.Response, error) {
|
||||
dotPath, err := DefaultDotDirectory(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return status(ctx, dotPath)
|
||||
}
|
||||
|
||||
func status(ctx context.Context, daemonPath string) (*api.Response, error) {
|
||||
client, err := api.NewClient(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error creating a new API client: %w", err)
|
||||
}
|
||||
addr := daemon.SocketAddress(daemonPath)
|
||||
_, err = os.Stat(strings.TrimPrefix(addr, "unix://"))
|
||||
if strings.HasPrefix(addr, "unix://") && err != nil {
|
||||
return nil, errDaemonNotRunning
|
||||
}
|
||||
if err := client.SetAddr(addr); err != nil {
|
||||
return nil, fmt.Errorf("Error when setting the client's address: %w", err)
|
||||
}
|
||||
// Because this is using the real lib it can pick up from stored locations
|
||||
// like the system keychain. Explicitly clear the token for now
|
||||
client.SetToken("")
|
||||
|
||||
req, err := client.NewRequest(ctx, "GET", "/status", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add(daemon.VersionHeaderKey, version.Get().VersionNumber())
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, 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 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.Address,
|
||||
"AuthToken Count": len(u.AuthTokens),
|
||||
}
|
||||
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 Full 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:", strings.Title(r.Name)),
|
||||
base.WrapMap(6, maxLength+6, nonAttributeMap),
|
||||
)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -19,6 +19,11 @@ type RefreshService struct {
|
||||
}
|
||||
|
||||
func NewRefreshService(ctx context.Context, r *Repository) (*RefreshService, error) {
|
||||
const op = "cache.NewRefreshService"
|
||||
switch {
|
||||
case util.IsNil(r):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "repository is nil")
|
||||
}
|
||||
return &RefreshService{repo: r}, nil
|
||||
}
|
||||
|
||||
|
||||
+34
-10
@@ -56,11 +56,11 @@ func NewRepository(ctx context.Context, conn *db.DB, idToAuthToken *sync.Map, ke
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) SaveError(ctx context.Context, u *user, resourceType resourceType, err error) error {
|
||||
const op = "cache.(Repository).StoreError"
|
||||
func (r *Repository) saveError(ctx context.Context, u *user, resourceType resourceType, err error) error {
|
||||
const op = "cache.(Repository).saveError"
|
||||
switch {
|
||||
case resourceType == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "resource type is empty")
|
||||
case !resourceType.valid():
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
||||
case err == nil:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "error is nil")
|
||||
case u == nil:
|
||||
@@ -68,9 +68,9 @@ func (r *Repository) SaveError(ctx context.Context, u *user, resourceType resour
|
||||
case u.Id == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
||||
}
|
||||
apiErr := &ApiError{
|
||||
apiErr := &apiError{
|
||||
UserId: u.Id,
|
||||
ResourceType: string(resourceType),
|
||||
ResourceType: resourceType,
|
||||
Error: err.Error(),
|
||||
}
|
||||
onConflict := db.OnConflict{
|
||||
@@ -83,13 +83,37 @@ func (r *Repository) SaveError(ctx context.Context, u *user, resourceType resour
|
||||
return nil
|
||||
}
|
||||
|
||||
type ApiError struct {
|
||||
UserId string `gorm:"primaryKey"`
|
||||
ResourceType string `gorm:"primaryKey"`
|
||||
func (r *Repository) lookupError(ctx context.Context, u *user, resourceType resourceType) (*apiError, error) {
|
||||
const op = "cache.(Repository).lookupError"
|
||||
switch {
|
||||
case !resourceType.valid():
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
||||
case u == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
case u.Id == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
||||
}
|
||||
apiErr := &apiError{
|
||||
UserId: u.Id,
|
||||
ResourceType: resourceType,
|
||||
}
|
||||
err := r.rw.LookupById(ctx, apiErr)
|
||||
if err != nil {
|
||||
if errors.IsNotFoundError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return apiErr, nil
|
||||
}
|
||||
|
||||
type apiError struct {
|
||||
UserId string `gorm:"primaryKey"`
|
||||
ResourceType resourceType `gorm:"primaryKey"`
|
||||
Error string
|
||||
CreateTime time.Time
|
||||
}
|
||||
|
||||
func (*ApiError) TableName() string {
|
||||
func (*apiError) TableName() string {
|
||||
return "api_error"
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package cache
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
@@ -112,6 +113,8 @@ func upsertRefreshToken(ctx context.Context, writer db.Writer, u *user, rt resou
|
||||
UserId: u.Id,
|
||||
ResourceType: rt,
|
||||
RefreshToken: tok,
|
||||
UpdateTime: time.Now(),
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
|
||||
switch tok {
|
||||
@@ -120,7 +123,7 @@ func upsertRefreshToken(ctx context.Context, writer db.Writer, u *user, rt resou
|
||||
default:
|
||||
onConflict := &db.OnConflict{
|
||||
Target: db.Columns{"user_id", "resource_type"},
|
||||
Action: db.SetColumns([]string{"refresh_token"}),
|
||||
Action: db.SetColumns([]string{"refresh_token", "update_time"}),
|
||||
}
|
||||
if err := writer.Create(ctx, refTok, db.WithOnConflict(onConflict)); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
@@ -150,6 +153,8 @@ type refreshToken struct {
|
||||
UserId string `gorm:"primaryKey"`
|
||||
ResourceType resourceType `gorm:"primaryKey"`
|
||||
RefreshToken RefreshTokenValue
|
||||
UpdateTime time.Time `gorm:"default:(strftime('%Y-%m-%d %H:%M:%f','now'))"`
|
||||
CreateTime time.Time `gorm:"default:(strftime('%Y-%m-%d %H:%M:%f','now'))"`
|
||||
}
|
||||
|
||||
func (*refreshToken) TableName() string {
|
||||
|
||||
@@ -95,7 +95,7 @@ func (r *Repository) refreshSessions(ctx context.Context, u *user, tokens map[Au
|
||||
}
|
||||
|
||||
if retErr != nil {
|
||||
if saveErr := r.SaveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
if saveErr := r.saveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func (r *Repository) fullFetchSessions(ctx context.Context, u *user, tokens map[
|
||||
}
|
||||
|
||||
if retErr != nil {
|
||||
if saveErr := r.SaveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
if saveErr := r.saveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func (r *Repository) refreshTargets(ctx context.Context, u *user, tokens map[Aut
|
||||
break
|
||||
}
|
||||
if retErr != nil {
|
||||
if saveErr := r.SaveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
if saveErr := r.saveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func (r *Repository) fullFetchTargets(ctx context.Context, u *user, tokens map[A
|
||||
break
|
||||
}
|
||||
if retErr != nil {
|
||||
if saveErr := r.SaveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
if saveErr := r.saveError(ctx, u, resourceType, retErr); saveErr != nil {
|
||||
return stderrors.Join(err, errors.Wrap(ctx, saveErr, op))
|
||||
}
|
||||
}
|
||||
|
||||
+52
-5
@@ -44,7 +44,7 @@ func sliceBasedAuthTokenBoundaryReader(s []*authtokens.AuthToken) BoundaryTokenR
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_SaveError(t *testing.T) {
|
||||
func TestRepository_saveError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
@@ -64,15 +64,62 @@ func TestRepository_SaveError(t *testing.T) {
|
||||
require.NoError(t, r.rw.Create(ctx, u))
|
||||
|
||||
t.Run("empty resource type", func(t *testing.T) {
|
||||
assert.ErrorContains(t, r.SaveError(ctx, u, "", testErr), "resource type is empty")
|
||||
assert.ErrorContains(t, r.saveError(ctx, u, "", testErr), "resource type is invalid")
|
||||
})
|
||||
t.Run("nil error", func(t *testing.T) {
|
||||
assert.ErrorContains(t, r.SaveError(ctx, u, testResource, nil), "error is nil")
|
||||
assert.ErrorContains(t, r.saveError(ctx, u, testResource, nil), "error is nil")
|
||||
})
|
||||
t.Run("nil user", func(t *testing.T) {
|
||||
assert.ErrorContains(t, r.SaveError(ctx, nil, testResource, testErr), "user is nil")
|
||||
assert.ErrorContains(t, r.saveError(ctx, nil, testResource, testErr), "user is nil")
|
||||
})
|
||||
t.Run("success", func(t *testing.T) {
|
||||
assert.NoError(t, r.SaveError(ctx, u, testResource, testErr))
|
||||
assert.NoError(t, r.saveError(ctx, u, testResource, testErr))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_lookupError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
r, err := NewRepository(ctx, s, &sync.Map{},
|
||||
mapBasedAuthTokenKeyringLookup(map[ringToken]*authtokens.AuthToken{}),
|
||||
sliceBasedAuthTokenBoundaryReader(nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
testResource := targetResourceType
|
||||
testErr := fmt.Errorf("test error for %q", testResource)
|
||||
|
||||
u := &user{
|
||||
Id: "u1",
|
||||
Address: "addr",
|
||||
}
|
||||
require.NoError(t, r.rw.Create(ctx, u))
|
||||
|
||||
t.Run("empty resource type", func(t *testing.T) {
|
||||
got, err := r.lookupError(ctx, u, "unknown")
|
||||
assert.ErrorContains(t, err, "resource type is invalid")
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
t.Run("nil user", func(t *testing.T) {
|
||||
got, err := r.lookupError(ctx, nil, testResource)
|
||||
assert.ErrorContains(t, err, "user is nil")
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
t.Run("empty user id", func(t *testing.T) {
|
||||
got, err := r.lookupError(ctx, &user{Address: "address"}, testResource)
|
||||
assert.ErrorContains(t, err, "user id is empty")
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
got, err := r.lookupError(ctx, u, sessionResourceType)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
t.Run("found", func(t *testing.T) {
|
||||
r.saveError(ctx, u, sessionResourceType, testErr)
|
||||
got, err := r.lookupError(ctx, u, sessionResourceType)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, got)
|
||||
})
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
// RefreshTokenStatus is the status of a resource token
|
||||
type RefreshTokenStatus struct {
|
||||
Age time.Duration
|
||||
LastUsed time.Duration
|
||||
}
|
||||
|
||||
type ErrorStatus struct {
|
||||
Error string
|
||||
LastReturned time.Duration
|
||||
}
|
||||
|
||||
// ResourceStatus contains the status of a specific resource type contained in
|
||||
// the cache for a specific user.
|
||||
type ResourceStatus struct {
|
||||
Name string
|
||||
Count int
|
||||
LastError *ErrorStatus
|
||||
RefreshToken *RefreshTokenStatus
|
||||
}
|
||||
|
||||
// AuthTokenStatus contains the status of an auth token tracked in the cache for
|
||||
// a specific user.
|
||||
type AuthTokenStatus struct {
|
||||
Id string
|
||||
KeyringReferences int
|
||||
KeyringlessReferences int
|
||||
}
|
||||
|
||||
// UserStatus contains the status of a specific user tracked by the cache
|
||||
type UserStatus struct {
|
||||
// The Id of the user this status is for
|
||||
Id string
|
||||
// The boundary address for this user
|
||||
Address string
|
||||
// The auth tokens used by this user to authenticate with the boundary instance
|
||||
AuthTokens []AuthTokenStatus
|
||||
// The resources tracked by the cache for this user
|
||||
Resources []ResourceStatus
|
||||
}
|
||||
|
||||
// Status contains the status of the cache.
|
||||
type Status struct {
|
||||
Users []UserStatus
|
||||
}
|
||||
|
||||
// StatusService provides a service that gives status information about the
|
||||
// cache.
|
||||
type StatusService struct {
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
// NewStatusService returns a new StatusService
|
||||
func NewStatusService(ctx context.Context, r *Repository) (*StatusService, error) {
|
||||
const op = "cache.NewStatusService"
|
||||
switch {
|
||||
case util.IsNil(r):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "repository is nil")
|
||||
}
|
||||
return &StatusService{repo: r}, nil
|
||||
}
|
||||
|
||||
// Status returns the Status of the cache repository
|
||||
func (s *StatusService) Status(ctx context.Context) (*Status, error) {
|
||||
const op = "cache.(StatusService).Status"
|
||||
ret := &Status{}
|
||||
users, err := s.repo.listUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
for _, u := range users {
|
||||
us := UserStatus{
|
||||
Id: u.Id,
|
||||
Address: u.Address,
|
||||
}
|
||||
|
||||
{
|
||||
toks, err := s.repo.listTokens(ctx, u)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
for _, t := range toks {
|
||||
ts := &AuthTokenStatus{
|
||||
Id: t.Id,
|
||||
}
|
||||
kt, err := s.repo.listKeyringTokens(ctx, t)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
ts.KeyringReferences = len(kt)
|
||||
if _, ok := s.repo.idToKeyringlessAuthToken.Load(t.Id); ok {
|
||||
ts.KeyringlessReferences = 1
|
||||
}
|
||||
us.AuthTokens = append(us.AuthTokens, *ts)
|
||||
}
|
||||
}
|
||||
|
||||
for _, rt := range []resourceType{targetResourceType, sessionResourceType} {
|
||||
ts, err := s.resourceStatus(ctx, u, rt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
us.Resources = append(us.Resources, *ts)
|
||||
}
|
||||
|
||||
ret.Users = append(ret.Users, us)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s *StatusService) resourceStatus(ctx context.Context, u *user, rt resourceType) (*ResourceStatus, error) {
|
||||
const op = "cache.(StatusService).resourceStatus"
|
||||
switch {
|
||||
case u == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
case u.Id == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
||||
case !rt.valid():
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
||||
}
|
||||
ret := ResourceStatus{
|
||||
Name: string(rt),
|
||||
}
|
||||
|
||||
refTok, err := s.refreshTokenStatus(ctx, u, rt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
ret.RefreshToken = refTok
|
||||
|
||||
errStatus, err := s.errorStatus(ctx, u, rt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
ret.LastError = errStatus
|
||||
|
||||
err = func() error {
|
||||
query := fmt.Sprintf("select count(*) from %s where user_id = @user_id", rt)
|
||||
r, err := s.repo.rw.Query(ctx, query, []any{sql.Named("user_id", u.Id)})
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
defer r.Close()
|
||||
r.Next()
|
||||
var count int
|
||||
if err := s.repo.rw.ScanRows(ctx, r, &count); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
ret.Count = count
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (s *StatusService) errorStatus(ctx context.Context, u *user, rt resourceType) (*ErrorStatus, error) {
|
||||
const op = "cache.(StatusService).errorStatus"
|
||||
switch {
|
||||
case u == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
case u.Id == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
||||
case !rt.valid():
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
||||
}
|
||||
apiErr, err := s.repo.lookupError(ctx, u, rt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if apiErr == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &ErrorStatus{
|
||||
Error: apiErr.Error,
|
||||
LastReturned: time.Since(apiErr.CreateTime),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *StatusService) refreshTokenStatus(ctx context.Context, u *user, rt resourceType) (*RefreshTokenStatus, error) {
|
||||
const op = "cache.(StatusService).refreshTokenStatus"
|
||||
switch {
|
||||
case u == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user is nil")
|
||||
case u.Id == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "user id is empty")
|
||||
case !rt.valid():
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "resource type is invalid")
|
||||
}
|
||||
refTok := &refreshToken{
|
||||
UserId: u.Id,
|
||||
ResourceType: rt,
|
||||
}
|
||||
if err := s.repo.rw.LookupById(ctx, refTok); err != nil {
|
||||
if errors.IsNotFoundError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
ret := &RefreshTokenStatus{
|
||||
LastUsed: time.Since(refTok.UpdateTime),
|
||||
Age: time.Since(refTok.CreateTime),
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authtokens"
|
||||
"github.com/hashicorp/boundary/api/sessions"
|
||||
"github.com/hashicorp/boundary/api/targets"
|
||||
cachedb "github.com/hashicorp/boundary/internal/clientcache/internal/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewStatusService(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("nil repo", func(t *testing.T) {
|
||||
s, err := NewStatusService(ctx, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, s)
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(nil), sliceBasedAuthTokenBoundaryReader(nil))
|
||||
require.NoError(t, err)
|
||||
|
||||
service, err := NewStatusService(ctx, r)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, service)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStatus(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, err := cachedb.Open(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
boundaryAddr := "address"
|
||||
u1 := &user{Id: "u1", Address: boundaryAddr}
|
||||
at1a := &authtokens.AuthToken{
|
||||
Id: "at_1a",
|
||||
Token: "at_1a_token",
|
||||
UserId: u1.Id,
|
||||
ExpirationTime: time.Now().Add(time.Minute),
|
||||
}
|
||||
at1b := &authtokens.AuthToken{
|
||||
Id: "at_1b",
|
||||
Token: "at_1b_token",
|
||||
UserId: u1.Id,
|
||||
ExpirationTime: time.Now().Add(time.Minute),
|
||||
}
|
||||
|
||||
u2 := &user{Id: "u2", Address: boundaryAddr}
|
||||
at2 := &authtokens.AuthToken{
|
||||
Id: "at_2",
|
||||
Token: "at_2_token",
|
||||
UserId: u2.Id,
|
||||
ExpirationTime: time.Now().Add(time.Minute),
|
||||
}
|
||||
|
||||
boundaryAuthTokens := []*authtokens.AuthToken{at1a, at1b, at2}
|
||||
atMap := map[ringToken]*authtokens.AuthToken{
|
||||
{k: "default", t: "default"}: at1a,
|
||||
{k: "default2", t: "default2"}: at1b,
|
||||
}
|
||||
r, err := NewRepository(ctx, s, &sync.Map{}, mapBasedAuthTokenKeyringLookup(atMap), sliceBasedAuthTokenBoundaryReader(boundaryAuthTokens))
|
||||
require.NoError(t, err)
|
||||
|
||||
ss, err := NewStatusService(ctx, r)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("empty status", func(t *testing.T) {
|
||||
got, err := ss.Status(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &Status{}, got)
|
||||
})
|
||||
|
||||
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
||||
KeyringType: "default",
|
||||
TokenName: "default",
|
||||
AuthTokenId: at1a.Id,
|
||||
}))
|
||||
require.NoError(t, r.AddKeyringToken(ctx, boundaryAddr, KeyringToken{
|
||||
KeyringType: "default2",
|
||||
TokenName: "default2",
|
||||
AuthTokenId: at1b.Id,
|
||||
}))
|
||||
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at1b.Token))
|
||||
require.NoError(t, r.AddRawToken(ctx, boundaryAddr, at2.Token))
|
||||
|
||||
t.Run("users with no resources", func(t *testing.T) {
|
||||
got, err := ss.Status(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &Status{
|
||||
Users: []UserStatus{
|
||||
{
|
||||
Id: u1.Id,
|
||||
Address: u1.Address,
|
||||
AuthTokens: []AuthTokenStatus{
|
||||
{
|
||||
Id: at1a.Id,
|
||||
KeyringReferences: 1,
|
||||
KeyringlessReferences: 0,
|
||||
},
|
||||
{
|
||||
Id: at1b.Id,
|
||||
KeyringReferences: 1,
|
||||
KeyringlessReferences: 1,
|
||||
},
|
||||
},
|
||||
Resources: []ResourceStatus{
|
||||
{
|
||||
Name: string(targetResourceType),
|
||||
Count: 0,
|
||||
},
|
||||
{
|
||||
Name: string(sessionResourceType),
|
||||
Count: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: u2.Id,
|
||||
Address: u2.Address,
|
||||
AuthTokens: []AuthTokenStatus{
|
||||
{
|
||||
Id: at2.Id,
|
||||
KeyringReferences: 0,
|
||||
KeyringlessReferences: 1,
|
||||
},
|
||||
},
|
||||
Resources: []ResourceStatus{
|
||||
{
|
||||
Name: string(targetResourceType),
|
||||
Count: 0,
|
||||
},
|
||||
{
|
||||
Name: string(sessionResourceType),
|
||||
Count: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, got)
|
||||
})
|
||||
|
||||
t.Run("users with errors", func(t *testing.T) {
|
||||
require.NoError(t, r.saveError(ctx, u1, targetResourceType, fmt.Errorf("test error")))
|
||||
|
||||
got, err := ss.Status(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
lastErr := got.Users[0].Resources[0].LastError
|
||||
assert.NotNil(t, lastErr)
|
||||
assert.Equal(t, "test error", lastErr.Error)
|
||||
assert.NotZero(t, lastErr.LastReturned)
|
||||
})
|
||||
|
||||
t.Run("users with resources", func(t *testing.T) {
|
||||
require.NoError(t, r.saveError(ctx, u1, targetResourceType, fmt.Errorf("test error")))
|
||||
|
||||
ts := []*targets.Target{
|
||||
target("1"),
|
||||
target("2"),
|
||||
target("3"),
|
||||
target("4"),
|
||||
}
|
||||
err = r.refreshTargets(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*targets.Target{ts}, [][]string{nil})))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = r.refreshTargets(ctx, u2, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithTargetRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*targets.Target{ts[:2]}, [][]string{nil})))
|
||||
require.NoError(t, err)
|
||||
|
||||
sess := []*sessions.Session{
|
||||
session("1"),
|
||||
session("2"),
|
||||
session("3"),
|
||||
}
|
||||
err := r.refreshSessions(ctx, u1, map[AuthToken]string{{Id: "id"}: "something"},
|
||||
WithSessionRetrievalFunc(testStaticResourceRetrievalFunc(t, [][]*sessions.Session{sess}, [][]string{nil})))
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := ss.Status(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, got.Users, 2)
|
||||
assert.Equal(t, Map(got.Users, func(i UserStatus) string {
|
||||
return i.Id
|
||||
}), []string{"u1", "u2"})
|
||||
|
||||
// User 1 status
|
||||
assert.Equal(t, Map(got.Users[0].AuthTokens, func(i AuthTokenStatus) string {
|
||||
return i.Id
|
||||
}), []string{"at_1a", "at_1b"})
|
||||
|
||||
assert.Equal(t, Map(got.Users[0].Resources, func(i ResourceStatus) string {
|
||||
return i.Name
|
||||
}), []string{string(targetResourceType), string(sessionResourceType)})
|
||||
|
||||
assert.Equal(t, Map(got.Users[0].Resources, func(i ResourceStatus) int {
|
||||
return i.Count
|
||||
}), []int{4, 3})
|
||||
|
||||
assert.Equal(t, Map(got.Users[0].Resources, func(i ResourceStatus) bool {
|
||||
return i.LastError == nil
|
||||
}), []bool{false, true}, "expected an error for target resource and none for session resource")
|
||||
|
||||
assert.Equal(t, Map(got.Users[0].Resources, func(i ResourceStatus) bool {
|
||||
return i.RefreshToken == nil
|
||||
}), []bool{false, false})
|
||||
|
||||
// User 2 status
|
||||
assert.Equal(t, Map(got.Users[1].AuthTokens, func(i AuthTokenStatus) string {
|
||||
return i.Id
|
||||
}), []string{"at_2"})
|
||||
|
||||
assert.Equal(t, Map(got.Users[1].Resources, func(i ResourceStatus) string {
|
||||
return i.Name
|
||||
}), []string{string(targetResourceType), string(sessionResourceType)})
|
||||
|
||||
assert.Equal(t, Map(got.Users[1].Resources, func(i ResourceStatus) int {
|
||||
return i.Count
|
||||
}), []int{2, 0})
|
||||
|
||||
assert.Equal(t, Map(got.Users[1].Resources, func(i ResourceStatus) bool {
|
||||
return i.LastError == nil
|
||||
}), []bool{true, true})
|
||||
|
||||
assert.Equal(t, Map(got.Users[1].Resources, func(i ResourceStatus) bool {
|
||||
return i.RefreshToken == nil
|
||||
}), []bool{false, true}, "targets expected to have a refresh token and sessions aren't")
|
||||
})
|
||||
}
|
||||
|
||||
// Map maps a slice of one type into a slice of another using the provided map
|
||||
// function
|
||||
func Map[T, U any](in []T, f func(T) U) []U {
|
||||
var ret []U
|
||||
for _, t := range in {
|
||||
ret = append(ret, f(t))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
+5
-1
@@ -189,8 +189,11 @@ func TestRefreshToken(t *testing.T) {
|
||||
ResourceType: targetResourceType,
|
||||
RefreshToken: "something",
|
||||
}
|
||||
before := time.Now().Truncate(1 * time.Millisecond)
|
||||
require.NoError(t, rw.Create(ctx, tok))
|
||||
require.NoError(t, rw.LookupById(ctx, tok))
|
||||
assert.GreaterOrEqual(t, tok.UpdateTime, before)
|
||||
assert.GreaterOrEqual(t, tok.CreateTime, before)
|
||||
assert.NotEmpty(t, tok.RefreshToken)
|
||||
})
|
||||
|
||||
@@ -208,8 +211,9 @@ func TestRefreshToken(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, rw.Create(ctx, tok))
|
||||
|
||||
tok.UpdateTime = time.Now().Add(-(24 * 365 * time.Hour))
|
||||
tok.RefreshToken = "updated"
|
||||
n, err := rw.Update(ctx, tok, []string{"RefreshToken"}, nil)
|
||||
n, err := rw.Update(ctx, tok, []string{"UpdateTime", "RefreshToken"}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, n)
|
||||
})
|
||||
|
||||
@@ -236,11 +236,17 @@ func (s *CacheServer) Serve(ctx context.Context, cmd Commander, opt ...Option) e
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
searchTargetsFn, err := newSearchHandlerFunc(ctx, repo)
|
||||
searchFn, err := newSearchHandlerFunc(ctx, repo)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
mux.HandleFunc("/v1/search", searchTargetsFn)
|
||||
mux.HandleFunc("/v1/search", searchFn)
|
||||
|
||||
statusFn, err := newStatusHandlerFunc(ctx, repo, l.Addr().String())
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
mux.HandleFunc("/v1/status", statusFn)
|
||||
|
||||
tokenFn, err := newTokenHandlerFunc(ctx, repo, tic)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
// RefreshTokenStatus is the status of a resource token
|
||||
type RefreshTokenStatus struct {
|
||||
Age time.Duration `json:"age,omitempty"`
|
||||
LastUsed time.Duration `json:"last_used,omitempty"`
|
||||
}
|
||||
|
||||
type ErrorStatus struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
LastReturned time.Duration `json:"last_returned,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceStatus contains the status of a specific resource type contained in
|
||||
// the cache for a specific user.
|
||||
type ResourceStatus struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Count int `json:"count"`
|
||||
LastError *ErrorStatus `json:"last_error,omitempty"`
|
||||
RefreshToken *RefreshTokenStatus `json:"refresh_token,omitempty"`
|
||||
}
|
||||
|
||||
// AuthTokenStatus contains the status of an auth token tracked in the cache for
|
||||
// a specific user.
|
||||
type AuthTokenStatus struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
KeyringReferences int `json:"keyring_references,omitempty"`
|
||||
KeyringlessReferences int `json:"keyringless_references,omitempty"`
|
||||
}
|
||||
|
||||
// UserStatus contains the status of a specific user tracked by the cache
|
||||
type UserStatus struct {
|
||||
// The Id of the user this status is for
|
||||
Id string `json:"id,omitempty"`
|
||||
// The boundary address for this user
|
||||
Address string `json:"address,omitempty"`
|
||||
// The auth tokens used by this user to authenticate with the boundary instance
|
||||
AuthTokens []AuthTokenStatus `json:"auth_tokens,omitempty"`
|
||||
// The resources tracked by the cache for this user
|
||||
Resources []ResourceStatus `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
// StatusResult is the struct returned to status requests.
|
||||
type StatusResult struct {
|
||||
Uptime time.Duration `json:"uptime,omitempty"`
|
||||
SocketAddress string `json:"socket_address,omitempty"`
|
||||
Users []UserStatus `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
func newStatusHandlerFunc(ctx context.Context, repo *cache.Repository, socketAddr string) (http.HandlerFunc, error) {
|
||||
const op = "daemon.newStatusHandlerFunc"
|
||||
switch {
|
||||
case util.IsNil(repo):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "repository is missing")
|
||||
}
|
||||
started := time.Now()
|
||||
|
||||
s, err := cache.NewStatusService(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
res, err := s.Status(ctx)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Match(errors.T(errors.InvalidParameter), err):
|
||||
writeError(w, err.Error(), http.StatusBadRequest)
|
||||
default:
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
if res == nil {
|
||||
writeError(w, "nil StatusResult generated", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
apiRes := toApiStatus(res, started, socketAddr)
|
||||
j, err := json.Marshal(apiRes)
|
||||
if err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// toApiStatus converts a domain status result to an api status result
|
||||
func toApiStatus(in *cache.Status, started time.Time, socketAddr string) *StatusResult {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := &StatusResult{
|
||||
Uptime: time.Since(started),
|
||||
SocketAddress: socketAddr,
|
||||
}
|
||||
|
||||
for _, inU := range in.Users {
|
||||
outU := UserStatus{
|
||||
Id: inU.Id,
|
||||
Address: inU.Address,
|
||||
}
|
||||
for _, inAt := range inU.AuthTokens {
|
||||
outU.AuthTokens = append(outU.AuthTokens, AuthTokenStatus{
|
||||
Id: inAt.Id,
|
||||
KeyringReferences: inAt.KeyringReferences,
|
||||
KeyringlessReferences: inAt.KeyringlessReferences,
|
||||
})
|
||||
}
|
||||
for _, inR := range inU.Resources {
|
||||
var outErr *ErrorStatus
|
||||
if inR.LastError != nil {
|
||||
outErr = &ErrorStatus{
|
||||
Error: inR.LastError.Error,
|
||||
LastReturned: inR.LastError.LastReturned,
|
||||
}
|
||||
}
|
||||
var outRefTok *RefreshTokenStatus
|
||||
if inR.RefreshToken != nil {
|
||||
outRefTok = &RefreshTokenStatus{
|
||||
Age: inR.RefreshToken.Age,
|
||||
LastUsed: inR.RefreshToken.LastUsed,
|
||||
}
|
||||
}
|
||||
outU.Resources = append(outU.Resources, ResourceStatus{
|
||||
Name: inR.Name,
|
||||
Count: inR.Count,
|
||||
LastError: outErr,
|
||||
RefreshToken: outRefTok,
|
||||
})
|
||||
}
|
||||
|
||||
out.Users = append(out.Users, outU)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -37,9 +37,28 @@ create table if not exists refresh_token(
|
||||
constraint only_known_resource_types_allowed,
|
||||
refresh_token text not null
|
||||
check (length(refresh_token) > 0),
|
||||
update_time timestamp not null default (strftime('%Y-%m-%d %H:%M:%f','now')),
|
||||
create_time timestamp not null default (strftime('%Y-%m-%d %H:%M:%f','now')),
|
||||
primary key (user_id, resource_type)
|
||||
);
|
||||
|
||||
create trigger immutable_columns_refresh_token before update on refresh_token
|
||||
for each row
|
||||
when
|
||||
new.create_time <> old.create_time
|
||||
begin
|
||||
select raise(abort, 'immutable column');
|
||||
end;
|
||||
|
||||
|
||||
create trigger update_time_column_refresh_token before update on refresh_token
|
||||
for each row
|
||||
when
|
||||
new.refresh_token <> old.refresh_token
|
||||
begin
|
||||
update refresh_token set update_time = datetime('now','localtime') where rowid == new.rowid;
|
||||
end;
|
||||
|
||||
-- Contains the boundary auth token
|
||||
create table if not exists auth_token (
|
||||
-- id is the boundary id of the auth token
|
||||
|
||||
@@ -30,6 +30,11 @@ func init() {
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon status"] = func() (cli.Command, error) {
|
||||
return &daemon.StatusCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["search"] = func() (cli.Command, error) {
|
||||
return &search.SearchCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
|
||||
@@ -32,6 +32,11 @@ func init() {
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon status"] = func() (cli.Command, error) {
|
||||
return &daemon.StatusCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["search"] = func() (cli.Command, error) {
|
||||
return &search.SearchCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
|
||||
@@ -30,6 +30,11 @@ func init() {
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon status"] = func() (cli.Command, error) {
|
||||
return &daemon.StatusCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["search"] = func() (cli.Command, error) {
|
||||
return &search.SearchCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
|
||||
@@ -32,6 +32,11 @@ func init() {
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["daemon status"] = func() (cli.Command, error) {
|
||||
return &daemon.StatusCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
}
|
||||
Commands["search"] = func() (cli.Command, error) {
|
||||
return &search.SearchCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
|
||||
Reference in new issue
Block a user