diff --git a/agent/agent.go b/agent/agent.go index 79a25a9ec0..1cab3accc1 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -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 diff --git a/agent/agent_test.go b/agent/agent_test.go index 282e397c4b..076ea75ce7 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -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 diff --git a/agent/consul/config.go b/agent/consul/config.go index 8e3bb92e4e..edc5423ca4 100644 --- a/agent/consul/config.go +++ b/agent/consul/config.go @@ -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 diff --git a/agent/consul/config_cloud.go b/agent/consul/config_cloud.go deleted file mode 100644 index 5b62574c81..0000000000 --- a/agent/consul/config_cloud.go +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package consul - -type CloudConfig struct { - ManagementToken string -} diff --git a/agent/consul/server.go b/agent/consul/server.go index 6de17e8319..89bcaf957c 100644 --- a/agent/consul/server.go +++ b/agent/consul/server.go @@ -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 diff --git a/agent/hcp/deps.go b/agent/hcp/deps.go index 7bf384747d..145532d411 100644 --- a/agent/hcp/deps.go +++ b/agent/hcp/deps.go @@ -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) } diff --git a/agent/hcp/manager.go b/agent/hcp/manager.go index a3664b0608..c5d3398499 100644 --- a/agent/hcp/manager.go +++ b/agent/hcp/manager.go @@ -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() diff --git a/agent/hcp/manager_test.go b/agent/hcp/manager_test.go index 8432e63ed5..2c29bc32c1 100644 --- a/agent/hcp/manager_test.go +++ b/agent/hcp/manager_test.go @@ -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()) diff --git a/agent/hcp/scada/mock_Provider.go b/agent/hcp/scada/mock_Provider.go index b9a0fd2d49..7e922cb21b 100644 --- a/agent/hcp/scada/mock_Provider.go +++ b/agent/hcp/scada/mock_Provider.go @@ -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) diff --git a/agent/hcp/scada/scada.go b/agent/hcp/scada/scada.go index 151e1b6862..c62f45908b 100644 --- a/agent/hcp/scada/scada.go +++ b/agent/hcp/scada/scada.go @@ -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 diff --git a/agent/hcp/scada/scada_test.go b/agent/hcp/scada/scada_test.go new file mode 100644 index 0000000000..0cebed1b93 --- /dev/null +++ b/agent/hcp/scada/scada_test.go @@ -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) + }) + } +} diff --git a/go.mod b/go.mod index d9ee0eb553..7957ea0e38 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a24941f42c..3d851eeb29 100644 --- a/go.sum +++ b/go.sum @@ -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=