consul/agent/hcp/manager.go

364 lines
8.7 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hcp
import (
"context"
"reflect"
"sync"
"time"
hcpclient "github.com/hashicorp/consul/agent/hcp/client"
"github.com/hashicorp/consul/agent/hcp/config"
"github.com/hashicorp/consul/agent/hcp/scada"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/go-hclog"
)
var (
defaultManagerMinInterval = 45 * time.Minute
defaultManagerMaxInterval = 75 * time.Minute
)
var _ Manager = (*HCPManager)(nil)
type ManagerConfig struct {
Client hcpclient.Client
CloudConfig config.CloudConfig
SCADAProvider scada.Provider
TelemetryProvider TelemetryProvider
StatusFn StatusCallback
// Idempotent function to upsert the HCP management token. This will be called periodically in
// the manager's main loop.
ManagementTokenUpserterFn ManagementTokenUpserter
ManagementTokenDeleterFn ManagementTokenDeleter
MinInterval time.Duration
MaxInterval time.Duration
Logger hclog.Logger
}
func (cfg *ManagerConfig) enabled() bool {
return cfg.Client != nil && cfg.StatusFn != nil
}
func (cfg *ManagerConfig) nextHeartbeat() time.Duration {
min := cfg.MinInterval
if min == 0 {
min = defaultManagerMinInterval
}
max := cfg.MaxInterval
if max == 0 {
max = defaultManagerMaxInterval
}
if max < min {
max = min
}
return min + lib.RandomStagger(max-min)
}
type StatusCallback func(context.Context) (hcpclient.ServerStatus, error)
type ManagementTokenUpserter func(name, secretId string) error
type ManagementTokenDeleter func(secretId string) error
//go:generate mockery --name Manager --with-expecter --inpackage
type Manager interface {
Start(context.Context) error
Stop() error
GetCloudConfig() config.CloudConfig
UpdateConfig(hcpclient.Client, config.CloudConfig)
}
type HCPManager struct {
logger hclog.Logger
running bool
runLock sync.RWMutex
cfg ManagerConfig
cfgMu sync.RWMutex
updateCh chan struct{}
stopCh chan struct{}
// testUpdateSent is set by unit tests to signal when the manager's status update has triggered
testUpdateSent chan struct{}
}
// NewManager returns a Manager initialized with the given configuration.
func NewManager(cfg ManagerConfig) *HCPManager {
return &HCPManager{
logger: cfg.Logger,
cfg: cfg,
updateCh: make(chan struct{}, 1),
}
}
// Start executes the logic for connecting to HCP and sending periodic server updates. If the
// manager has been previously started, it will not start again.
func (m *HCPManager) Start(ctx context.Context) error {
// Check if the manager has already started
changed := m.setRunning(true)
if !changed {
m.logger.Trace("HCP manager already started")
return nil
}
var err error
m.logger.Info("HCP manager starting")
// Update and start the SCADA provider
err = m.startSCADAProvider()
if err != nil {
m.logger.Error("failed to start scada provider", "error", err)
m.setRunning(false)
return err
}
// Update and start the telemetry provider to enable the HCP metrics sink
if err := m.startTelemetryProvider(ctx); err != nil {
m.logger.Error("failed to update telemetry config provider", "error", err)
m.setRunning(false)
return err
}
// immediately send initial update
select {
case <-ctx.Done():
m.setRunning(false)
return nil
case <-m.stopCh:
return nil
case <-m.updateCh: // empty the update chan if there is a queued update to prevent repeated update in main loop
err = m.sendUpdate()
if err != nil {
m.setRunning(false)
return err
}
default:
err = m.sendUpdate()
if err != nil {
m.setRunning(false)
return err
}
}
// main loop
go func() {
for {
m.cfgMu.RLock()
cfg := m.cfg
m.cfgMu.RUnlock()
// Check for configured management token from HCP and upsert it if found
if hcpManagement := cfg.CloudConfig.ManagementToken; len(hcpManagement) > 0 {
if cfg.ManagementTokenUpserterFn != nil {
upsertTokenErr := cfg.ManagementTokenUpserterFn("HCP Management Token", hcpManagement)
if upsertTokenErr != nil {
m.logger.Error("failed to upsert HCP management token", "err", upsertTokenErr)
}
}
}
nextUpdate := cfg.nextHeartbeat()
if err != nil {
m.logger.Error("failed to send server status to HCP", "err", err, "next_heartbeat", nextUpdate.String())
}
select {
case <-ctx.Done():
m.setRunning(false)
return
case <-m.stopCh:
return
case <-m.updateCh:
err = m.sendUpdate()
case <-time.After(nextUpdate):
err = m.sendUpdate()
}
}
}()
return err
}
func (m *HCPManager) startSCADAProvider() error {
provider := m.cfg.SCADAProvider
if provider == nil {
return nil
}
// Update the SCADA provider configuration with HCP configurations
m.logger.Debug("updating scada provider with HCP configuration")
err := provider.UpdateHCPConfig(m.cfg.CloudConfig)
if err != nil {
m.logger.Error("failed to update scada provider with HCP configuration", "err", err)
return err
}
// Update the SCADA provider metadata
provider.UpdateMeta(map[string]string{
"consul_server_id": string(m.cfg.CloudConfig.NodeID),
})
// Start the SCADA provider
err = provider.Start()
if err != nil {
return err
}
return nil
}
func (m *HCPManager) startTelemetryProvider(ctx context.Context) error {
if m.cfg.TelemetryProvider == nil || reflect.ValueOf(m.cfg.TelemetryProvider).IsNil() {
return nil
}
m.cfg.TelemetryProvider.Start(ctx, &HCPProviderCfg{
HCPClient: m.cfg.Client,
HCPConfig: &m.cfg.CloudConfig,
})
return nil
}
func (m *HCPManager) GetCloudConfig() config.CloudConfig {
m.cfgMu.RLock()
defer m.cfgMu.RUnlock()
return m.cfg.CloudConfig
}
func (m *HCPManager) UpdateConfig(client hcpclient.Client, cloudCfg config.CloudConfig) {
m.cfgMu.Lock()
// Save original values
originalCfg := m.cfg.CloudConfig
originalClient := m.cfg.Client
// Update with new values
m.cfg.Client = client
m.cfg.CloudConfig = cloudCfg
m.cfgMu.Unlock()
// Send update if already running and values were updated
if m.isRunning() && (originalClient != client || originalCfg != cloudCfg) {
m.SendUpdate()
}
}
func (m *HCPManager) SendUpdate() {
m.logger.Debug("HCP triggering status update")
select {
case m.updateCh <- struct{}{}:
// trigger update
default:
// if chan is full then there is already an update triggered that will soon
// be acted on so don't bother blocking.
}
}
// TODO: we should have retried on failures here with backoff but take into
// account that if a new update is triggered while we are still retrying we
// should not start another retry loop. Something like have a "dirty" flag which
// we mark on first PushUpdate and then a retry timer as well as the interval
// and a "isRetrying" state or something so that we attempt to send update, but
// then fetch fresh info on each attempt to send so if we are already in a retry
// backoff a new push is a no-op.
func (m *HCPManager) sendUpdate() error {
m.cfgMu.RLock()
cfg := m.cfg
m.cfgMu.RUnlock()
if !cfg.enabled() {
return nil
}
if m.testUpdateSent != nil {
defer func() {
select {
case m.testUpdateSent <- struct{}{}:
default:
}
}()
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
s, err := cfg.StatusFn(ctx)
if err != nil {
return err
}
return cfg.Client.PushServerStatus(ctx, &s)
}
func (m *HCPManager) isRunning() bool {
m.runLock.RLock()
defer m.runLock.RUnlock()
return m.running
}
// setRunning sets the running status of the manager to the given value. If the
// given value is the same as the current running status, it returns false. If
// current status is updated to the given status, it returns true.
func (m *HCPManager) setRunning(r bool) bool {
m.runLock.Lock()
defer m.runLock.Unlock()
if m.running == r {
return false
}
// Initialize or close the stop channel depending what running status
// we're transitioning to. Channel must be initialized on start since
// a provider can be stopped and started multiple times.
if r {
m.stopCh = make(chan struct{})
} else {
close(m.stopCh)
}
m.running = r
return true
}
// Stop stops the manager's main loop that sends updates
// and stops the SCADA provider and telemetry provider.
func (m *HCPManager) Stop() error {
changed := m.setRunning(false)
if !changed {
m.logger.Trace("HCP manager already stopped")
return nil
}
m.logger.Info("HCP manager stopping")
m.cfgMu.RLock()
defer m.cfgMu.RUnlock()
if m.cfg.SCADAProvider != nil {
m.cfg.SCADAProvider.Stop()
}
if m.cfg.TelemetryProvider != nil && !reflect.ValueOf(m.cfg.TelemetryProvider).IsNil() {
m.cfg.TelemetryProvider.Stop()
}
if m.cfg.ManagementTokenDeleterFn != nil && m.cfg.CloudConfig.ManagementToken != "" {
err := m.cfg.ManagementTokenDeleterFn(m.cfg.CloudConfig.ManagementToken)
if err != nil {
return err
}
}
m.logger.Info("HCP manager stopped")
return nil
}