[CC-7041] Update and start the SCADA provider in HCP manager (#19976)

* Update SCADA provider version

Also update mocks for SCADA provider.

* Create SCADA provider w/o HCP config, then update

Adds a placeholder config option to allow us to initialize a SCADA provider
without the HCP configuration. Also adds an update method to then add the
HCP configuration. We need this to be able to eventually always register a
SCADA listener at startup before the HCP config values are known.

* Pass cloud configuration to HCP manager

Save the entire cloud configuration and pass it to the HCP
manager.

* Update and start SCADA provider in HCP manager

Move config updating and starting to the HCP manager. The HCP manager
will eventually be responsible for all processes that contribute
to linking to HCP.
This commit is contained in:
Melissa Kam 2024-01-08 09:49:29 -06:00 committed by GitHub
parent 0d57acc549
commit 5dc8eabcce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 300 additions and 49 deletions

View File

@ -896,17 +896,6 @@ func (a *Agent) Start(ctx context.Context) error {
}()
}
if a.scadaProvider != nil {
a.scadaProvider.UpdateMeta(map[string]string{
"consul_server_id": string(a.config.NodeID),
})
if err = a.scadaProvider.Start(); err != nil {
a.baseDeps.Logger.Error("scada provider failed to start, some HashiCorp Cloud Platform functionality has been disabled",
"error", err, "resource_id", a.config.Cloud.ResourceID)
}
}
return nil
}
@ -1598,7 +1587,7 @@ func newConsulConfig(runtimeCfg *config.RuntimeConfig, logger hclog.Logger) (*co
cfg.RequestLimitsWriteRate = runtimeCfg.RequestLimitsWriteRate
cfg.Locality = runtimeCfg.StructLocality()
cfg.Cloud.ManagementToken = runtimeCfg.Cloud.ManagementToken
cfg.Cloud = runtimeCfg.Cloud
cfg.Reporting.License.Enabled = runtimeCfg.Reporting.License.Enabled

View File

@ -6343,6 +6343,7 @@ func TestAgent_scadaProvider(t *testing.T) {
pvd.EXPECT().Listen(scada.CAPCoreAPI.Capability()).Return(l, nil).Once()
pvd.EXPECT().Stop().Return(nil).Once()
pvd.EXPECT().SessionStatus().Return("test")
pvd.EXPECT().UpdateHCPConfig(mock.Anything).Return(nil).Once()
a := TestAgent{
OverrideDeps: func(deps *BaseDeps) {
deps.HCP.Provider = pvd

View File

@ -16,6 +16,7 @@ import (
"github.com/hashicorp/consul/agent/checks"
consulrate "github.com/hashicorp/consul/agent/consul/rate"
hcpconfig "github.com/hashicorp/consul/agent/hcp/config"
"github.com/hashicorp/consul/agent/structs"
libserf "github.com/hashicorp/consul/lib/serf"
"github.com/hashicorp/consul/tlsutil"
@ -442,7 +443,7 @@ type Config struct {
Locality *structs.Locality
Cloud CloudConfig
Cloud hcpconfig.CloudConfig
Reporting Reporting

View File

@ -1,8 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package consul
type CloudConfig struct {
ManagementToken string
}

View File

@ -586,9 +586,11 @@ func NewServer(config *Config, flat Deps, externalGRPCServer *grpc.Server,
})
s.hcpManager = hcp.NewManager(hcp.ManagerConfig{
Client: flat.HCP.Client,
StatusFn: s.hcpServerStatus(flat),
Logger: logger.Named("hcp_manager"),
CloudConfig: s.config.Cloud,
Client: flat.HCP.Client,
StatusFn: s.hcpServerStatus(flat),
Logger: logger.Named("hcp_manager"),
SCADAProvider: flat.HCP.Provider,
})
var recorder *middleware.RequestRecorder

View File

@ -32,7 +32,7 @@ func NewDeps(cfg config.CloudConfig, logger hclog.Logger) (Deps, error) {
return Deps{}, fmt.Errorf("failed to init client: %w", err)
}
provider, err := scada.New(cfg, logger.Named("scada"))
provider, err := scada.New(logger.Named("scada"))
if err != nil {
return Deps{}, fmt.Errorf("failed to init scada: %w", err)
}

View File

@ -9,6 +9,8 @@ import (
"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"
)
@ -19,7 +21,9 @@ var (
)
type ManagerConfig struct {
Client hcpclient.Client
Client hcpclient.Client
CloudConfig config.CloudConfig
SCADAProvider scada.Provider
StatusFn StatusCallback
MinInterval time.Duration
@ -83,6 +87,15 @@ func (m *Manager) Run(ctx context.Context) {
var err error
m.logger.Debug("HCP manager starting")
// Update and start the SCADA provider
err = m.startSCADAProvider()
if err != nil {
// Log the error but continue starting the manager. The SCADA provider
// could potentially be updated later with a working configuration.
m.logger.Error("scada provider failed to start, some HashiCorp Cloud Platform functionality has been disabled",
"error", err)
}
// immediately send initial update
select {
case <-ctx.Done():
@ -116,6 +129,34 @@ func (m *Manager) Run(ctx context.Context) {
}
}
func (m *Manager) 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 *Manager) UpdateConfig(cfg ManagerConfig) {
m.cfgMu.Lock()
defer m.cfgMu.Unlock()

View File

@ -9,6 +9,8 @@ import (
"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/go-hclog"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -22,10 +24,26 @@ func TestManager_Run(t *testing.T) {
}
updateCh := make(chan struct{}, 1)
client.EXPECT().PushServerStatus(mock.Anything, &hcpclient.ServerStatus{ID: t.Name()}).Return(nil).Once()
cloudCfg := config.CloudConfig{
ResourceID: "organization/85702e73-8a3d-47dc-291c-379b783c5804/project/8c0547c0-10e8-1ea2-dffe-384bee8da634/hashicorp.consul.global-network-manager.cluster/test",
NodeID: "node-1",
}
scadaM := scada.NewMockProvider(t)
scadaM.EXPECT().UpdateHCPConfig(cloudCfg).Return(nil)
scadaM.EXPECT().UpdateMeta(
map[string]string{
"consul_server_id": string(cloudCfg.NodeID),
},
).Return()
scadaM.EXPECT().Start().Return(nil)
mgr := NewManager(ManagerConfig{
Client: client,
Logger: hclog.New(&hclog.LoggerOptions{Output: io.Discard}),
StatusFn: statusF,
Client: client,
Logger: hclog.New(&hclog.LoggerOptions{Output: io.Discard}),
StatusFn: statusF,
CloudConfig: cloudCfg,
SCADAProvider: scadaM,
})
mgr.testUpdateSent = updateCh
ctx, cancel := context.WithCancel(context.Background())

View File

@ -1,12 +1,13 @@
// Code generated by mockery v2.20.0. DO NOT EDIT.
// Code generated by mockery v2.38.0. DO NOT EDIT.
package scada
import (
net "net"
config "github.com/hashicorp/consul/agent/hcp/config"
mock "github.com/stretchr/testify/mock"
net "net"
provider "github.com/hashicorp/hcp-scada-provider"
time "time"
@ -121,6 +122,10 @@ func (_c *MockProvider_DeleteMeta_Call) RunAndReturn(run func(...string)) *MockP
func (_m *MockProvider) GetMeta() map[string]string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetMeta")
}
var r0 map[string]string
if rf, ok := ret.Get(0).(func() map[string]string); ok {
r0 = rf()
@ -164,6 +169,10 @@ func (_c *MockProvider_GetMeta_Call) RunAndReturn(run func() map[string]string)
func (_m *MockProvider) LastError() (time.Time, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for LastError")
}
var r0 time.Time
var r1 error
if rf, ok := ret.Get(0).(func() (time.Time, error)); ok {
@ -215,6 +224,10 @@ func (_c *MockProvider_LastError_Call) RunAndReturn(run func() (time.Time, error
func (_m *MockProvider) Listen(capability string) (net.Listener, error) {
ret := _m.Called(capability)
if len(ret) == 0 {
panic("no return value specified for Listen")
}
var r0 net.Listener
var r1 error
if rf, ok := ret.Get(0).(func(string) (net.Listener, error)); ok {
@ -269,6 +282,10 @@ func (_c *MockProvider_Listen_Call) RunAndReturn(run func(string) (net.Listener,
func (_m *MockProvider) SessionStatus() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for SessionStatus")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
@ -310,6 +327,10 @@ func (_c *MockProvider_SessionStatus_Call) RunAndReturn(run func() string) *Mock
func (_m *MockProvider) Start() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Start")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
@ -351,6 +372,10 @@ func (_c *MockProvider_Start_Call) RunAndReturn(run func() error) *MockProvider_
func (_m *MockProvider) Stop() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Stop")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
@ -388,6 +413,98 @@ func (_c *MockProvider_Stop_Call) RunAndReturn(run func() error) *MockProvider_S
return _c
}
// UpdateConfig provides a mock function with given fields: _a0
func (_m *MockProvider) UpdateConfig(_a0 *provider.Config) error {
ret := _m.Called(_a0)
if len(ret) == 0 {
panic("no return value specified for UpdateConfig")
}
var r0 error
if rf, ok := ret.Get(0).(func(*provider.Config) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockProvider_UpdateConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateConfig'
type MockProvider_UpdateConfig_Call struct {
*mock.Call
}
// UpdateConfig is a helper method to define mock.On call
// - _a0 *provider.Config
func (_e *MockProvider_Expecter) UpdateConfig(_a0 interface{}) *MockProvider_UpdateConfig_Call {
return &MockProvider_UpdateConfig_Call{Call: _e.mock.On("UpdateConfig", _a0)}
}
func (_c *MockProvider_UpdateConfig_Call) Run(run func(_a0 *provider.Config)) *MockProvider_UpdateConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*provider.Config))
})
return _c
}
func (_c *MockProvider_UpdateConfig_Call) Return(_a0 error) *MockProvider_UpdateConfig_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockProvider_UpdateConfig_Call) RunAndReturn(run func(*provider.Config) error) *MockProvider_UpdateConfig_Call {
_c.Call.Return(run)
return _c
}
// UpdateHCPConfig provides a mock function with given fields: cfg
func (_m *MockProvider) UpdateHCPConfig(cfg config.CloudConfig) error {
ret := _m.Called(cfg)
if len(ret) == 0 {
panic("no return value specified for UpdateHCPConfig")
}
var r0 error
if rf, ok := ret.Get(0).(func(config.CloudConfig) error); ok {
r0 = rf(cfg)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockProvider_UpdateHCPConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateHCPConfig'
type MockProvider_UpdateHCPConfig_Call struct {
*mock.Call
}
// UpdateHCPConfig is a helper method to define mock.On call
// - cfg config.CloudConfig
func (_e *MockProvider_Expecter) UpdateHCPConfig(cfg interface{}) *MockProvider_UpdateHCPConfig_Call {
return &MockProvider_UpdateHCPConfig_Call{Call: _e.mock.On("UpdateHCPConfig", cfg)}
}
func (_c *MockProvider_UpdateHCPConfig_Call) Run(run func(cfg config.CloudConfig)) *MockProvider_UpdateHCPConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(config.CloudConfig))
})
return _c
}
func (_c *MockProvider_UpdateHCPConfig_Call) Return(_a0 error) *MockProvider_UpdateHCPConfig_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockProvider_UpdateHCPConfig_Call) RunAndReturn(run func(config.CloudConfig) error) *MockProvider_UpdateHCPConfig_Call {
_c.Call.Return(run)
return _c
}
// UpdateMeta provides a mock function with given fields: _a0
func (_m *MockProvider) UpdateMeta(_a0 map[string]string) {
_m.Called(_a0)
@ -421,13 +538,12 @@ func (_c *MockProvider_UpdateMeta_Call) RunAndReturn(run func(map[string]string)
return _c
}
type mockConstructorTestingTNewMockProvider interface {
// NewMockProvider creates a new instance of MockProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockProvider(t interface {
mock.TestingT
Cleanup(func())
}
// NewMockProvider creates a new instance of MockProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockProvider(t mockConstructorTestingTNewMockProvider) *MockProvider {
}) *MockProvider {
mock := &MockProvider{}
mock.Mock.Test(t)

View File

@ -11,7 +11,8 @@ import (
"github.com/hashicorp/go-hclog"
libscada "github.com/hashicorp/hcp-scada-provider"
"github.com/hashicorp/hcp-scada-provider/capability"
"github.com/hashicorp/hcp-sdk-go/resource"
cloud "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models"
hcpcfg "github.com/hashicorp/hcp-sdk-go/config"
)
// Provider is the interface used in the rest of Consul core when using SCADA, it is aliased here to the same interface
@ -21,34 +22,72 @@ import (
//go:generate mockery --name Provider --with-expecter --inpackage
type Provider interface {
libscada.SCADAProvider
UpdateHCPConfig(cfg config.CloudConfig) error
}
const (
scadaConsulServiceKey = "consul"
)
func New(cfg config.CloudConfig, logger hclog.Logger) (Provider, error) {
resource, err := resource.FromString(cfg.ResourceID)
if err != nil {
return nil, fmt.Errorf("failed to parse cloud resource_id: %w", err)
type scadaProvider struct {
libscada.SCADAProvider
logger hclog.Logger
}
// New returns an initialized SCADA provider with a zero configuration.
// It can listen but cannot start until UpdateHCPConfig is called with
// a configuration that provides credentials to contact HCP.
func New(logger hclog.Logger) (*scadaProvider, error) {
// Create placeholder resource link
resourceLink := cloud.HashicorpCloudLocationLink{
Type: "no-op",
ID: "no-op",
Location: &cloud.HashicorpCloudLocationLocation{},
}
hcpConfig, err := cfg.HCPConfig()
// Configure with an empty HCP configuration
hcpConfig, err := hcpcfg.NewHCPConfig(hcpcfg.WithoutBrowserLogin())
if err != nil {
return nil, fmt.Errorf("failed to build HCPConfig: %w", err)
return nil, fmt.Errorf("failed to configure SCADA provider: %w", err)
}
pvd, err := libscada.New(&libscada.Config{
Service: scadaConsulServiceKey,
HCPConfig: hcpConfig,
Resource: *resource.Link(),
Resource: resourceLink,
Logger: logger,
})
if err != nil {
return nil, err
}
return pvd, nil
return &scadaProvider{pvd, logger}, nil
}
// UpdateHCPConfig updates the SCADA provider with the given HCP
// configurations.
func (p *scadaProvider) UpdateHCPConfig(cfg config.CloudConfig) error {
resource, err := cfg.Resource()
if err != nil {
return err
}
hcpCfg, err := cfg.HCPConfig()
if err != nil {
return err
}
err = p.UpdateConfig(&libscada.Config{
Service: scadaConsulServiceKey,
HCPConfig: hcpCfg,
Resource: *resource.Link(),
Logger: p.logger,
})
if err != nil {
return err
}
return nil
}
// IsCapability takes a net.Addr and returns true if it is a SCADA capability.Addr

View File

@ -0,0 +1,52 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package scada
import (
"testing"
"github.com/hashicorp/consul/agent/hcp/config"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require"
)
func TestUpdateHCPConfig(t *testing.T) {
for name, tc := range map[string]struct {
cfg config.CloudConfig
expectedErr string
}{
"Success": {
cfg: config.CloudConfig{
ResourceID: "organization/85702e73-8a3d-47dc-291c-379b783c5804/project/8c0547c0-10e8-1ea2-dffe-384bee8da634/hashicorp.consul.global-network-manager.cluster/test",
ClientID: "test",
ClientSecret: "test",
},
},
"Empty": {
cfg: config.CloudConfig{},
expectedErr: "could not parse resource: unexpected number of tokens 1",
},
"InvalidResource": {
cfg: config.CloudConfig{
ResourceID: "invalid",
},
expectedErr: "could not parse resource: unexpected number of tokens 1",
},
} {
t.Run(name, func(t *testing.T) {
// Create a provider
p, err := New(hclog.NewNullLogger())
require.NoError(t, err)
// Update the provider
err = p.UpdateHCPConfig(tc.cfg)
if tc.expectedErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr)
return
}
require.NoError(t, err)
})
}
}

2
go.mod
View File

@ -66,7 +66,7 @@ require (
github.com/hashicorp/hcdiag v0.5.1
github.com/hashicorp/hcl v1.0.0
github.com/hashicorp/hcl/v2 v2.14.1
github.com/hashicorp/hcp-scada-provider v0.2.3
github.com/hashicorp/hcp-scada-provider v0.2.4-0.20231215224332-eb6c5d2e36d2
github.com/hashicorp/hcp-sdk-go v0.73.0
github.com/hashicorp/hil v0.0.0-20200423225030-a18a1cd20038
github.com/hashicorp/memberlist v0.5.0

4
go.sum
View File

@ -577,8 +577,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.14.1 h1:x0BpjfZ+CYdbiz+8yZTQ+gdLO7IXvOut7Da+XJayx34=
github.com/hashicorp/hcl/v2 v2.14.1/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
github.com/hashicorp/hcp-scada-provider v0.2.3 h1:AarYR+/Pcv+cMvPdAlb92uOBmZfEH6ny4+DT+4NY2VQ=
github.com/hashicorp/hcp-scada-provider v0.2.3/go.mod h1:ZFTgGwkzNv99PLQjTsulzaCplCzOTBh0IUQsPKzrQFo=
github.com/hashicorp/hcp-scada-provider v0.2.4-0.20231215224332-eb6c5d2e36d2 h1:qvvooL5OqWc6yiExmSQpHUUS1UZq0di/9OXh5Mwv6Xc=
github.com/hashicorp/hcp-scada-provider v0.2.4-0.20231215224332-eb6c5d2e36d2/go.mod h1:ZFTgGwkzNv99PLQjTsulzaCplCzOTBh0IUQsPKzrQFo=
github.com/hashicorp/hcp-sdk-go v0.73.0 h1:KjizNN/53nu4YkrDZ24xKjy4EgFt9b3nk1vgfAmgwUk=
github.com/hashicorp/hcp-sdk-go v0.73.0/go.mod h1:k/wgUsKSa2OzWBM5/Pj5ST0YwFGpgC4O5EtCq882jSw=
github.com/hashicorp/hil v0.0.0-20200423225030-a18a1cd20038 h1:n9J0rwVWXDpNd5iZnwY7w4WZyq53/rROeI7OVvLW8Ok=