1
0
vault-redux/vault/hcp_link/link.go
2024-01-02 10:36:20 -08:00

386 lines
11 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hcp_link
import (
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
link "github.com/hashicorp/hcp-link"
linkConfig "github.com/hashicorp/hcp-link/pkg/config"
scada "github.com/hashicorp/hcp-scada-provider"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/hashicorp/vault/vault"
"github.com/hashicorp/vault/vault/hcp_link/capabilities"
"github.com/hashicorp/vault/vault/hcp_link/capabilities/api_capability"
"github.com/hashicorp/vault/vault/hcp_link/capabilities/link_control"
"github.com/hashicorp/vault/vault/hcp_link/capabilities/meta"
"github.com/hashicorp/vault/vault/hcp_link/capabilities/node_status"
"github.com/hashicorp/vault/vault/hcp_link/internal"
vaultVersion "github.com/hashicorp/vault/version"
)
const (
SetLinkStatusCadence = 5 * time.Second
// metaDataNodeStatus is used to set the Scada provider metadata status
// to indicate if Vault is in active or standby status
metaDataNodeStatus = "link.node_status"
standbyStatus = "STANDBY"
activeStatus = "ACTIVE"
perfStandbyStatus = "PERF-STANDBY"
)
var (
// genericScadaConnectionError is used when Vault fails to fetch
// last connection error from Scada Provider
genericScadaConnectionError = errors.New("unable to establish a connection with HCP")
invalidClientCredentials = errors.New("failed to get access token: oauth2: cannot fetch token: 401 Unauthorized")
)
type HCPLinkVault struct {
l sync.RWMutex
LinkStatus internal.WrappedCoreHCPLinkStatus
scadaConfig *scada.Config
linkConfig *linkConfig.Config
link link.Link
logger hclog.Logger
capabilities map[string]capabilities.Capability
stopCh chan struct{}
running bool
}
func NewHCPLink(linkConf *configutil.HCPLinkConfig, core *vault.Core, logger hclog.Logger) (*HCPLinkVault, error) {
if linkConf == nil {
return nil, nil
}
scadaLogger := logger.Named("scada")
scadaConfig, err := internal.NewScadaConfig(linkConf, scadaLogger)
if err != nil {
return nil, fmt.Errorf("failed to instantiate SCADA config, %w", err)
}
// setting the link status in core, as link config is not nil
// At this point scada provider has not been started yet
// After starting scada provider, we need to use
// scadaProvider.SessionStatus() to get the status of the connection
core.SetHCPLinkStatus(
buildConnectionErrorMessage(scada.SessionStatusDisconnected, scada.ErrProviderNotStarted.Error(), time.Now()),
scadaConfig.Resource.Location.ProjectID,
)
// Creating SCADA provider
scadaProvider, err := scada.New(scadaConfig)
if err != nil {
return nil, fmt.Errorf("failed to instantiate SCADA provider: %w", err)
}
resource := scadaConfig.Resource
hcpConfig := scadaConfig.HCPConfig
version := vaultVersion.Version
// initializing node status reporter. This capability is configured by link lib.
statusReporter := &node_status.NodeStatusReporter{
NodeStatusGetter: core,
}
nodeID, err := core.LoadNodeID()
if err != nil {
return nil, fmt.Errorf("failed to get nodeID, %w", err)
}
// Compile the Link config
var conf *linkConfig.Config
conf, err = internal.NewLinkConfig(
nodeID,
version,
resource,
scadaProvider,
hcpConfig,
statusReporter,
logger,
)
if err != nil {
return nil, fmt.Errorf("failed to instantiate Link library config: %w", err)
}
// Create a Link library instance
hcpLink, err := link.New(conf)
if err != nil {
return nil, fmt.Errorf("failed to instantiate Link library: %w", err)
}
hcpLinkCaps, err := initializeCapabilities(linkConf, scadaConfig, scadaProvider, core, logger)
if err != nil {
return nil, fmt.Errorf("failed to initialize capabilities: %w", err)
}
hcpLinkVault := &HCPLinkVault{
LinkStatus: core,
scadaConfig: scadaConfig,
linkConfig: conf,
link: hcpLink,
capabilities: hcpLinkCaps,
stopCh: make(chan struct{}),
logger: logger,
}
// Start hcpLink and ScadaProvider
err = hcpLinkVault.start()
if err != nil {
return nil, fmt.Errorf("failed to start hcp link, %w", err)
}
return hcpLinkVault, nil
}
func initializeCapabilities(linkConf *configutil.HCPLinkConfig, scadaConfig *scada.Config, scadaProvider scada.SCADAProvider, core *vault.Core, logger hclog.Logger) (map[string]capabilities.Capability, error) {
hcpLinkCaps := make(map[string]capabilities.Capability, 0)
metaCap := meta.NewHCPLinkMetaService(scadaProvider, core, logger)
hcpLinkCaps[capabilities.MetaCapability] = metaCap
// Initializing API and link-control capabilities
var retErr *multierror.Error
if linkConf.EnableAPICapability {
apiCap, err := api_capability.NewAPICapability(scadaConfig, scadaProvider, core, logger)
if err != nil {
retErr = multierror.Append(retErr, fmt.Errorf("failed to instantiate API capability, %w", err))
}
hcpLinkCaps[capabilities.APICapability] = apiCap
// link control capability is tied to api capability
linkControlCap := link_control.NewHCPLinkControlService(scadaProvider, core, apiCap.PurgePolicy, logger)
hcpLinkCaps[capabilities.LinkControlCapability] = linkControlCap
}
// Initializing Passthrough capability
if linkConf.EnablePassThroughCapability {
apiPassCap, err := api_capability.NewAPIPassThroughCapability(scadaProvider, core, logger)
if err != nil {
retErr = multierror.Append(retErr, fmt.Errorf("failed to instantiate PassThrough capability, %w", err))
}
hcpLinkCaps[capabilities.APIPassThroughCapability] = apiPassCap
}
return hcpLinkCaps, retErr.ErrorOrNil()
}
// Start the connection regardless if the node is in seal mode or not
func (h *HCPLinkVault) start() error {
h.l.Lock()
defer h.l.Unlock()
if h.running {
return nil
}
if h.linkConfig == nil {
return fmt.Errorf("hcpLink config has not been provided")
}
scadaProvider := h.linkConfig.SCADAProvider
if scadaProvider == nil {
return fmt.Errorf("the reference to Scada provider in hcp link config is nil")
}
// Start both the Link functionality and the provider
if err := h.link.Start(); err != nil {
return fmt.Errorf("failed to start Link functionality, %w", err)
}
if err := scadaProvider.Start(); err != nil {
return fmt.Errorf("failed to start SCADA provider, %w", err)
}
// The connection should have been established between Vault and HCP
// Update core with the status
h.LinkStatus.SetHCPLinkStatus(h.GetConnectionStatusMessage(h.GetScadaSessionStatus()), h.getResourceID())
// Running capabilities
err := h.RunCapabilities()
if err != nil {
h.logger.Error("failed to start HCP link capabilities", "error", err.Error())
}
go h.reportStatus()
h.running = true
h.logger.Info("established connection to HCP")
return nil
}
// runs in a goroutine and in every 5 seconds, it sets the link status in Core
// such that a user could query the health of the connection via seal-status
// API. In addition, it checks replication status of Vault and sets that in
// Scada provider metadata status
func (h *HCPLinkVault) reportStatus() {
h.l.RLock()
stopCh := h.stopCh
h.l.RUnlock()
var currentNodeStatus string
ticker := time.NewTicker(SetLinkStatusCadence)
defer ticker.Stop()
for {
// Check for a shutdown
select {
case <-stopCh:
h.logger.Trace("returning from reporting link/node status")
return
case <-ticker.C:
// setting the HCP link status in core in this cadence
h.LinkStatus.SetHCPLinkStatus(
h.GetConnectionStatusMessage(h.GetScadaSessionStatus()),
h.getResourceID(),
)
// if node is in standby mode, set Scada metadata to indicate that
var nodeStatus string
standby, perfStandby := h.LinkStatus.StandbyStates()
switch {
case perfStandby:
nodeStatus = perfStandbyStatus
case standby:
nodeStatus = standbyStatus
default:
nodeStatus = activeStatus
}
// Only update SCADA session metadata if status has changed
if currentNodeStatus != nodeStatus {
currentNodeStatus = nodeStatus
h.linkConfig.SCADAProvider.UpdateMeta(map[string]string{metaDataNodeStatus: currentNodeStatus})
}
}
}
}
func buildConnectionErrorMessage(scadaStatus, errMsg string, errTime time.Time) string {
return fmt.Sprintf("%s since %s; error: %v", scadaStatus, errTime.Format(time.RFC3339Nano), errMsg)
}
// GetConnectionStatusMessage returns a meaningful message about connection
// status. If Scada connection is anything other than "connected", it will
// get the LastError from ScadaProvider, and builds a message with the
// scada session status, error time and error message, and returns the message.
func (h *HCPLinkVault) GetConnectionStatusMessage(scadaStatus string) string {
if scadaStatus == scada.SessionStatusConnected {
return scadaStatus
}
// HCP connectivity team is going to unify "connecting" with "waiting"
// statuses later. For simplicity, we unify the two until Scada
// provider unifies them
if scadaStatus == scada.SessionStatusWaiting {
scadaStatus = scada.SessionStatusConnecting
}
// There are two other states "connecting" and "disconnected"
// For those, there could have been an error with the connection
var errToReturn string
errTime, err := h.linkConfig.SCADAProvider.LastError()
if err == nil {
err = genericScadaConnectionError
errTime = time.Now()
}
switch {
case strings.Contains(err.Error(), scada.ErrPermissionDenied.Error()):
errToReturn = scada.ErrPermissionDenied.Error()
case strings.Contains(err.Error(), invalidClientCredentials.Error()), strings.Contains(err.Error(), scada.ErrInvalidCredentials.Error()):
errToReturn = scada.ErrInvalidCredentials.Error()
default:
errToReturn = genericScadaConnectionError.Error()
}
return buildConnectionErrorMessage(scadaStatus, errToReturn, errTime)
}
func (h *HCPLinkVault) getResourceID() string {
if h.scadaConfig != nil {
return h.scadaConfig.Resource.ID
}
return ""
}
func (h *HCPLinkVault) GetScadaSessionStatus() string {
if h.linkConfig != nil && h.linkConfig.SCADAProvider != nil {
return h.linkConfig.SCADAProvider.SessionStatus()
}
return scada.SessionStatusDisconnected
}
func (h *HCPLinkVault) Shutdown() error {
h.l.Lock()
defer h.l.Unlock()
if !h.running {
return nil
}
if h.stopCh != nil {
close(h.stopCh)
h.stopCh = nil
}
h.logger.Info("tearing down connection to HCP")
var retErr *multierror.Error
// stopping capabilities
for capName, capability := range h.capabilities {
err := capability.Stop()
if err != nil {
retErr = multierror.Append(retErr, fmt.Errorf("failed to close capability %s, %w", capName, err))
}
}
// updating metaDataNodeStatus before stopping link
h.linkConfig.SCADAProvider.UpdateMeta(map[string]string{metaDataNodeStatus: ""})
// stopping hcp link
err := h.link.Stop()
if err != nil {
retErr = multierror.Append(err, fmt.Errorf("failed to stop link %w", err))
}
h.link = nil
// stopping scada provider
err = h.linkConfig.SCADAProvider.Stop()
if err != nil {
retErr = multierror.Append(err, fmt.Errorf("failed to stop scada provider %w", err))
}
// setting the link status in Vault
h.LinkStatus.SetHCPLinkStatus(h.GetConnectionStatusMessage(h.GetScadaSessionStatus()), h.getResourceID())
h.running = false
return retErr.ErrorOrNil()
}
func (h *HCPLinkVault) RunCapabilities() error {
var retErr *multierror.Error
for capName, capability := range h.capabilities {
err := capability.Start()
if err != nil {
retErr = multierror.Append(retErr, fmt.Errorf("failed to start capability %s, %v", capName, err))
}
}
return retErr.ErrorOrNil()
}