// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package hcp import ( "fmt" "io" "testing" "time" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "golang.org/x/net/context" "github.com/hashicorp/go-hclog" 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/sdk/testutil" ) func TestManager_Start(t *testing.T) { client := hcpclient.NewMockClient(t) statusF := func(ctx context.Context) (hcpclient.ServerStatus, error) { return hcpclient.ServerStatus{ID: t.Name()}, nil } upsertManagementTokenCalled := make(chan struct{}, 1) upsertManagementTokenF := func(name, secretID string) error { upsertManagementTokenCalled <- struct{}{} return nil } updateCh := make(chan struct{}, 1) client.EXPECT().PushServerStatus(mock.Anything, &hcpclient.ServerStatus{ID: t.Name()}).Return(nil).Once() cloudCfg := config.CloudConfig{ ResourceID: "resource-id", NodeID: "node-1", ManagementToken: "fake-token", } scadaM := scada.NewMockProvider(t) scadaM.EXPECT().UpdateHCPConfig(cloudCfg).Return(nil).Once() scadaM.EXPECT().UpdateMeta( map[string]string{ "consul_server_id": string(cloudCfg.NodeID), }, ).Return().Once() scadaM.EXPECT().Start().Return(nil) telemetryM := NewMockTelemetryProvider(t) telemetryM.EXPECT().Start( mock.Anything, &HCPProviderCfg{ HCPClient: client, HCPConfig: &cloudCfg, }, ).Return(nil).Once() mgr := NewManager( ManagerConfig{ Logger: hclog.New(&hclog.LoggerOptions{Output: io.Discard}), StatusFn: statusF, ManagementTokenUpserterFn: upsertManagementTokenF, SCADAProvider: scadaM, TelemetryProvider: telemetryM, }, ) mgr.testUpdateSent = updateCh ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) mgr.UpdateConfig(client, cloudCfg) mgr.Start(ctx) select { case <-updateCh: case <-time.After(time.Second): require.Fail(t, "manager did not send update in expected time") } select { case <-upsertManagementTokenCalled: case <-time.After(time.Second): require.Fail(t, "manager did not upsert management token in expected time") } // Make sure after manager has stopped no more statuses are pushed. cancel() client.AssertExpectations(t) } func TestManager_StartMultipleTimes(t *testing.T) { client := hcpclient.NewMockClient(t) statusF := func(ctx context.Context) (hcpclient.ServerStatus, error) { return hcpclient.ServerStatus{ID: t.Name()}, nil } 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", ManagementToken: "fake-token", } mgr := NewManager( ManagerConfig{ Logger: hclog.New(&hclog.LoggerOptions{Output: io.Discard}), StatusFn: statusF, }, ) mgr.testUpdateSent = updateCh ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) // Start the manager twice concurrently, expect only one update mgr.UpdateConfig(client, cloudCfg) go mgr.Start(ctx) go mgr.Start(ctx) select { case <-updateCh: case <-time.After(time.Second): require.Fail(t, "manager did not send update in expected time") } select { case <-updateCh: require.Fail(t, "manager sent an update when not expected") case <-time.After(time.Second): } // Try start the manager again, still don't expect an update since already running mgr.Start(ctx) select { case <-updateCh: require.Fail(t, "manager sent an update when not expected") case <-time.After(time.Second): } } func TestManager_UpdateConfig(t *testing.T) { client := hcpclient.NewMockClient(t) statusF := func(ctx context.Context) (hcpclient.ServerStatus, error) { return hcpclient.ServerStatus{ID: t.Name()}, nil } updateCh := make(chan struct{}, 1) 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", } mgr := NewManager( ManagerConfig{ Logger: hclog.New(&hclog.LoggerOptions{Output: io.Discard}), StatusFn: statusF, CloudConfig: cloudCfg, Client: client, }, ) mgr.testUpdateSent = updateCh ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) // Start the manager, expect an initial status update client.EXPECT().PushServerStatus(mock.Anything, &hcpclient.ServerStatus{ID: t.Name()}).Return(nil).Once() mgr.Start(ctx) select { case <-updateCh: case <-time.After(time.Second): require.Fail(t, "manager did not send update in expected time") } // Update the cloud configuration, expect a status update client.EXPECT().PushServerStatus(mock.Anything, &hcpclient.ServerStatus{ID: t.Name()}).Return(nil).Once() updatedCfg := cloudCfg updatedCfg.ManagementToken = "token" mgr.UpdateConfig(client, updatedCfg) select { case <-updateCh: case <-time.After(time.Second): require.Fail(t, "manager did not send update in expected time") } // Update the client, expect a status update updatedClient := hcpclient.NewMockClient(t) updatedClient.EXPECT().PushServerStatus(mock.Anything, &hcpclient.ServerStatus{ID: t.Name()}).Return(nil).Once() mgr.UpdateConfig(updatedClient, updatedCfg) select { case <-updateCh: case <-time.After(time.Second): require.Fail(t, "manager did not send update in expected time") } // Update with the same values, don't expect a status update mgr.UpdateConfig(updatedClient, updatedCfg) select { case <-updateCh: require.Fail(t, "manager sent an update when not expected") case <-time.After(time.Second): } } func TestManager_SendUpdate(t *testing.T) { client := hcpclient.NewMockClient(t) statusF := func(ctx context.Context) (hcpclient.ServerStatus, error) { return hcpclient.ServerStatus{ID: t.Name()}, nil } updateCh := make(chan struct{}, 1) // Expect two calls, once during run startup and again when SendUpdate is called client.EXPECT().PushServerStatus(mock.Anything, &hcpclient.ServerStatus{ID: t.Name()}).Return(nil).Twice() mgr := NewManager( ManagerConfig{ Client: client, Logger: hclog.New(&hclog.LoggerOptions{Output: io.Discard}), StatusFn: statusF, }, ) mgr.testUpdateSent = updateCh ctx, cancel := context.WithCancel(context.Background()) defer cancel() mgr.Start(ctx) select { case <-updateCh: case <-time.After(time.Second): require.Fail(t, "manager did not send update in expected time") } mgr.SendUpdate() select { case <-updateCh: case <-time.After(time.Second): require.Fail(t, "manager did not send update in expected time") } client.AssertExpectations(t) } func TestManager_SendUpdate_Periodic(t *testing.T) { client := hcpclient.NewMockClient(t) statusF := func(ctx context.Context) (hcpclient.ServerStatus, error) { return hcpclient.ServerStatus{ID: t.Name()}, nil } updateCh := make(chan struct{}, 1) // Expect two calls, once during run startup and again when SendUpdate is called client.EXPECT().PushServerStatus(mock.Anything, &hcpclient.ServerStatus{ID: t.Name()}).Return(nil).Twice() mgr := NewManager( ManagerConfig{ Client: client, Logger: hclog.New(&hclog.LoggerOptions{Output: io.Discard}), StatusFn: statusF, MaxInterval: time.Second, MinInterval: 100 * time.Millisecond, }, ) mgr.testUpdateSent = updateCh ctx, cancel := context.WithCancel(context.Background()) defer cancel() mgr.Start(ctx) select { case <-updateCh: case <-time.After(time.Second): require.Fail(t, "manager did not send update in expected time") } select { case <-updateCh: case <-time.After(time.Second): require.Fail(t, "manager did not send update in expected time") } client.AssertExpectations(t) } func TestManager_Stop(t *testing.T) { client := hcpclient.NewMockClient(t) // Configure status functions called in sendUpdate statusF := func(ctx context.Context) (hcpclient.ServerStatus, error) { return hcpclient.ServerStatus{ID: t.Name()}, nil } updateCh := make(chan struct{}, 1) client.EXPECT().PushServerStatus(mock.Anything, &hcpclient.ServerStatus{ID: t.Name()}).Return(nil).Twice() // Configure management token creation and cleanup token := "test-token" upsertManagementTokenCalled := make(chan struct{}, 1) upsertManagementTokenF := func(name, secretID string) error { upsertManagementTokenCalled <- struct{}{} if secretID != token { return fmt.Errorf("expected token %q, got %q", token, secretID) } return nil } deleteManagementTokenCalled := make(chan struct{}, 1) deleteManagementTokenF := func(secretID string) error { deleteManagementTokenCalled <- struct{}{} if secretID != token { return fmt.Errorf("expected token %q, got %q", token, secretID) } return nil } // Configure the SCADA provider scadaM := scada.NewMockProvider(t) scadaM.EXPECT().UpdateHCPConfig(mock.Anything).Return(nil).Once() scadaM.EXPECT().UpdateMeta(mock.Anything).Return().Once() scadaM.EXPECT().Start().Return(nil).Once() scadaM.EXPECT().Stop().Return(nil).Once() // Configure the telemetry provider telemetryM := NewMockTelemetryProvider(t) telemetryM.EXPECT().Start(mock.Anything, mock.Anything).Return(nil).Once() telemetryM.EXPECT().Stop().Return().Once() // Configure manager with all its dependencies mgr := NewManager( ManagerConfig{ Logger: testutil.Logger(t), StatusFn: statusF, Client: client, ManagementTokenUpserterFn: upsertManagementTokenF, ManagementTokenDeleterFn: deleteManagementTokenF, SCADAProvider: scadaM, TelemetryProvider: telemetryM, CloudConfig: config.CloudConfig{ ManagementToken: token, }, }, ) mgr.testUpdateSent = updateCh ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) // Start the manager err := mgr.Start(ctx) require.NoError(t, err) select { case <-updateCh: case <-time.After(time.Second): require.Fail(t, "manager did not send update in expected time") } select { case <-upsertManagementTokenCalled: case <-time.After(time.Second): require.Fail(t, "manager did not create token in expected time") } // Send an update to ensure the manager is running in its main loop mgr.SendUpdate() select { case <-updateCh: case <-time.After(time.Second): require.Fail(t, "manager did not send update in expected time") } // Stop the manager err = mgr.Stop() require.NoError(t, err) // Validate that the management token delete function is called select { case <-deleteManagementTokenCalled: case <-time.After(time.Millisecond * 100): require.Fail(t, "manager did not create token in expected time") } // Send an update, expect no update since manager is stopped mgr.SendUpdate() select { case <-updateCh: require.Fail(t, "manager sent update after stopped") case <-time.After(time.Second): } }