Files
threshold/internal/clientcache/internal/cache/status.go
T

242 lines
6.3 KiB
Go

// 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
}
type BoundaryStatus struct {
// The boundary address for this user
Address string
CachingSupported CacheSupport
LastSupportCheck time.Duration
}
// 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
BoundaryStatus BoundaryStatus
// 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,
}
cacheSupported, err := s.repo.cacheSupportState(ctx, u)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
us.BoundaryStatus = BoundaryStatus{
Address: u.Address,
CachingSupported: cacheSupported.supported,
}
if cacheSupported.lastChecked != nil && !cacheSupported.lastChecked.IsZero() {
us.BoundaryStatus.LastSupportCheck = time.Since(*cacheSupported.lastChecked)
}
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{resolvableAliasResourceType, 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 fk_user_id = @user_id", rt.ColumnName())
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)
}
if refTok.RefreshToken == sentinelNoRefreshToken {
return nil, nil
}
ret := &RefreshTokenStatus{
LastUsed: time.Since(refTok.UpdateTime),
Age: time.Since(refTok.CreateTime),
}
return ret, nil
}