mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 21:52:02 +08:00
338 lines
11 KiB
Go
338 lines
11 KiB
Go
/*
|
|
Copyright 2021 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
|
"github.com/gravitational/trace"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/exp/slices"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/types"
|
|
clients "github.com/gravitational/teleport/lib/cloud"
|
|
"github.com/gravitational/teleport/lib/cloud/aws"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
discovery "github.com/gravitational/teleport/lib/srv/discovery/common"
|
|
dbfetchers "github.com/gravitational/teleport/lib/srv/discovery/fetchers/db"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
// startReconciler starts reconciler that registers/unregisters proxied
|
|
// databases according to the up-to-date list of database resources and
|
|
// databases imported from the cloud.
|
|
func (s *Server) startReconciler(ctx context.Context) error {
|
|
reconciler, err := services.NewReconciler(services.ReconcilerConfig{
|
|
Matcher: s.matcher,
|
|
GetCurrentResources: s.getResources,
|
|
GetNewResources: s.monitoredDatabases.get,
|
|
OnCreate: s.onCreate,
|
|
OnUpdate: s.onUpdate,
|
|
OnDelete: s.onDelete,
|
|
Log: s.log,
|
|
})
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-s.reconcileCh:
|
|
if err := reconciler.Reconcile(ctx); err != nil {
|
|
s.log.WithError(err).Error("Failed to reconcile.")
|
|
} else if s.cfg.OnReconcile != nil {
|
|
s.cfg.OnReconcile(s.getProxiedDatabases())
|
|
}
|
|
case <-ctx.Done():
|
|
s.log.Debug("Reconciler done.")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// startResourceWatcher starts watching changes to database resources and
|
|
// registers/unregisters the proxied databases accordingly.
|
|
func (s *Server) startResourceWatcher(ctx context.Context) (*services.DatabaseWatcher, error) {
|
|
if len(s.cfg.ResourceMatchers) == 0 {
|
|
s.log.Debug("Not starting database resource watcher.")
|
|
return nil, nil
|
|
}
|
|
s.log.Debug("Starting database resource watcher.")
|
|
watcher, err := services.NewDatabaseWatcher(ctx, services.DatabaseWatcherConfig{
|
|
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
|
Component: teleport.ComponentDatabase,
|
|
Log: s.log,
|
|
Client: s.cfg.AccessPoint,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
go func() {
|
|
defer s.log.Debug("Database resource watcher done.")
|
|
defer watcher.Close()
|
|
for {
|
|
select {
|
|
case databases := <-watcher.DatabasesC:
|
|
s.monitoredDatabases.setResources(databases)
|
|
select {
|
|
case s.reconcileCh <- struct{}{}:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return watcher, nil
|
|
}
|
|
|
|
// startCloudWatcher starts fetching cloud databases according to the
|
|
// selectors and register/unregister them appropriately.
|
|
func (s *Server) startCloudWatcher(ctx context.Context) error {
|
|
awsFetchers, err := dbfetchers.MakeAWSFetchers(s.cfg.CloudClients, s.cfg.AWSMatchers)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
azureFetchers, err := dbfetchers.MakeAzureFetchers(s.cfg.CloudClients, s.cfg.AzureMatchers)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
watcher, err := discovery.NewWatcher(ctx, discovery.WatcherConfig{
|
|
Fetchers: append(awsFetchers, azureFetchers...),
|
|
Log: logrus.WithField(trace.Component, "watcher:cloud"),
|
|
})
|
|
if err != nil {
|
|
if trace.IsNotFound(err) {
|
|
s.log.Debugf("Not starting cloud database watcher: %v.", err)
|
|
return nil
|
|
}
|
|
return trace.Wrap(err)
|
|
}
|
|
go watcher.Start()
|
|
go func() {
|
|
defer s.log.Debug("Cloud database watcher done.")
|
|
for {
|
|
select {
|
|
case resources := <-watcher.ResourcesC():
|
|
databases, err := resources.AsDatabases()
|
|
if err == nil {
|
|
s.monitoredDatabases.setCloud(databases)
|
|
} else {
|
|
s.log.WithError(err).Warnf("Failed to convert resources to databases.")
|
|
}
|
|
select {
|
|
case s.reconcileCh <- struct{}{}:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// getResources returns proxied databases as resources.
|
|
func (s *Server) getResources() types.ResourcesWithLabelsMap {
|
|
return s.getProxiedDatabases().AsResources().ToMap()
|
|
}
|
|
|
|
// onCreate is called by reconciler when a new database is created.
|
|
func (s *Server) onCreate(ctx context.Context, resource types.ResourceWithLabels) error {
|
|
database, ok := resource.(types.Database)
|
|
if !ok {
|
|
return trace.BadParameter("expected types.Database, got %T", resource)
|
|
}
|
|
|
|
if s.monitoredDatabases.isDiscoveryResource(database) {
|
|
s.cfg.discoveryResourceChecker.check(ctx, database)
|
|
}
|
|
return s.registerDatabase(ctx, database)
|
|
}
|
|
|
|
// onUpdate is called by reconciler when an already proxied database is updated.
|
|
func (s *Server) onUpdate(ctx context.Context, resource types.ResourceWithLabels) error {
|
|
database, ok := resource.(types.Database)
|
|
if !ok {
|
|
return trace.BadParameter("expected types.Database, got %T", resource)
|
|
}
|
|
return s.updateDatabase(ctx, database)
|
|
}
|
|
|
|
// onDelete is called by reconciler when a proxied database is deleted.
|
|
func (s *Server) onDelete(ctx context.Context, resource types.ResourceWithLabels) error {
|
|
database, ok := resource.(types.Database)
|
|
if !ok {
|
|
return trace.BadParameter("expected types.Database, got %T", resource)
|
|
}
|
|
return s.unregisterDatabase(ctx, database)
|
|
}
|
|
|
|
// matcher is used by reconciler to check if database matches selectors.
|
|
func (s *Server) matcher(resource types.ResourceWithLabels) bool {
|
|
database, ok := resource.(types.Database)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// In the case of databases discovered by this database server, matchers
|
|
// should be skipped.
|
|
if s.monitoredDatabases.isCloud(database) {
|
|
return true // Cloud fetchers return only matching databases.
|
|
}
|
|
|
|
// Database resources created via CLI, API, or discovery service are
|
|
// filtered by resource matchers.
|
|
return services.MatchResourceLabels(s.cfg.ResourceMatchers, database)
|
|
}
|
|
|
|
// discoveryResourceChecker defines an interface for checking database
|
|
// resources created by the discovery service.
|
|
type discoveryResourceChecker interface {
|
|
// check performs required checks on provided database resource before it
|
|
// gets registered.
|
|
check(ctx context.Context, database types.Database)
|
|
}
|
|
|
|
// cloudCredentialsChecker is a discoveryResourceChecker for validating cloud
|
|
// credentials against the incoming discovery resources.
|
|
type cloudCredentialsChecker struct {
|
|
cloudClients clients.Clients
|
|
resourceMatchers []services.ResourceMatcher
|
|
log *logrus.Entry
|
|
cache *utils.FnCache
|
|
}
|
|
|
|
func newCloudCrednentialsChecker(ctx context.Context, cloudClients clients.Clients, resourceMatchers []services.ResourceMatcher) (discoveryResourceChecker, error) {
|
|
cache, err := utils.NewFnCache(utils.FnCacheConfig{
|
|
TTL: 10 * time.Minute,
|
|
Context: ctx,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &cloudCredentialsChecker{
|
|
cloudClients: cloudClients,
|
|
resourceMatchers: resourceMatchers,
|
|
log: logrus.WithField(trace.Component, teleport.ComponentDatabase),
|
|
cache: cache,
|
|
}, nil
|
|
}
|
|
|
|
// check performs some quick checks to see whether this database agent can handle
|
|
// the incoming database (likely created by discovery service), and logs a
|
|
// warning with suggestions for this situation.
|
|
func (c *cloudCredentialsChecker) check(ctx context.Context, database types.Database) {
|
|
if database.Origin() != types.OriginCloud {
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case database.IsAWSHosted():
|
|
c.checkAWS(ctx, database)
|
|
case database.IsAzure():
|
|
c.checkAzure(ctx, database)
|
|
default:
|
|
c.log.Debugf("Database %q has unknown cloud type %q.", database.GetName(), database.GetType())
|
|
}
|
|
}
|
|
|
|
func (c *cloudCredentialsChecker) checkAWS(ctx context.Context, database types.Database) {
|
|
identity, err := utils.FnCacheGet(ctx, c.cache, types.CloudAWS, func(ctx context.Context) (aws.Identity, error) {
|
|
client, err := c.cloudClients.GetAWSSTSClient("")
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return aws.GetIdentityWithClient(ctx, client)
|
|
})
|
|
if err != nil {
|
|
c.warn(err, database, "Failed to get AWS caller identity when checking a database created by the discovery service.")
|
|
return
|
|
}
|
|
|
|
meta := database.GetAWS()
|
|
if meta.AccountID != "" && meta.AccountID != identity.GetAccountID() {
|
|
c.warn(nil, database, fmt.Sprintf("The database agent's caller identity and discovered database %q have different AWS account IDs (%s vs %s).",
|
|
database.GetName(),
|
|
identity.GetAccountID(),
|
|
meta.AccountID,
|
|
))
|
|
return
|
|
}
|
|
}
|
|
|
|
func (c *cloudCredentialsChecker) checkAzure(ctx context.Context, database types.Database) {
|
|
allSubIDs, err := utils.FnCacheGet(ctx, c.cache, types.CloudAzure, func(ctx context.Context) ([]string, error) {
|
|
client, err := c.cloudClients.GetAzureSubscriptionClient()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return client.ListSubscriptionIDs(ctx)
|
|
})
|
|
if err != nil {
|
|
c.warn(err, database, "Failed to get Azure subscription IDs when checking a database created by the discovery service.")
|
|
return
|
|
}
|
|
|
|
rid, err := arm.ParseResourceID(database.GetAzure().ResourceID)
|
|
if err != nil {
|
|
c.log.Warnf("Failed to parse resource ID of database %q: %v.", database.GetName(), err)
|
|
return
|
|
}
|
|
|
|
if !slices.Contains(allSubIDs, rid.SubscriptionID) {
|
|
c.warn(nil, database, fmt.Sprintf("The discovered database %q is in a subscription (ID: %s) that the database agent does not have access to.",
|
|
database.GetName(),
|
|
rid.SubscriptionID,
|
|
))
|
|
return
|
|
}
|
|
}
|
|
|
|
func (c *cloudCredentialsChecker) warn(err error, database types.Database, msg string) {
|
|
log := c.log.WithField("database", database)
|
|
if err != nil {
|
|
log = log.WithField("error", err.Error())
|
|
}
|
|
|
|
logLevel := logrus.InfoLevel
|
|
if c.isWildcardMatcher() {
|
|
logLevel = logrus.WarnLevel
|
|
}
|
|
log.Logf(logLevel, "%s You can update \"db_service.resources\" section of this agent's config file to filter out unwanted resources (see https://goteleport.com/docs/database-access/reference/configuration/ for more details). If this database is intended to be handled by this agent, please verify that valid cloud credentials are configured for the agent.", msg)
|
|
}
|
|
|
|
func (c *cloudCredentialsChecker) isWildcardMatcher() bool {
|
|
if len(c.resourceMatchers) != 1 {
|
|
return false
|
|
}
|
|
|
|
wildcardLabels := c.resourceMatchers[0].Labels[types.Wildcard]
|
|
return len(wildcardLabels) == 1 && wildcardLabels[0] == types.Wildcard
|
|
}
|