From 5dc8eabcce702e49316a23e1af5514672b862423 Mon Sep 17 00:00:00 2001 From: Melissa Kam <3768460+mkam@users.noreply.github.com> Date: Mon, 8 Jan 2024 09:49:29 -0600 Subject: [PATCH] [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. --- agent/agent.go | 13 +-- agent/agent_test.go | 1 + agent/consul/config.go | 3 +- agent/consul/config_cloud.go | 8 -- agent/consul/server.go | 8 +- agent/hcp/deps.go | 2 +- agent/hcp/manager.go | 43 +++++++++- agent/hcp/manager_test.go | 24 +++++- agent/hcp/scada/mock_Provider.go | 132 +++++++++++++++++++++++++++++-- agent/hcp/scada/scada.go | 57 ++++++++++--- agent/hcp/scada/scada_test.go | 52 ++++++++++++ go.mod | 2 +- go.sum | 4 +- 13 files changed, 300 insertions(+), 49 deletions(-) delete mode 100644 agent/consul/config_cloud.go create mode 100644 agent/hcp/scada/scada_test.go 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=