diff --git a/internal/command/command_service.go b/internal/command/command_service.go index e49644e996..e34f4cd53a 100644 --- a/internal/command/command_service.go +++ b/internal/command/command_service.go @@ -208,10 +208,8 @@ func (cs *CommandService) CreateConnection( resource *mpi.Resource, ) (*mpi.CreateConnectionResponse, error) { correlationID := logger.GetCorrelationID(ctx) - - // Only send a resource update message if instances other than the agent instance are found if len(resource.GetInstances()) <= 1 { - return nil, errors.New("waiting for data plane instances to be found before sending create connection request") + slog.InfoContext(ctx, "No Data Plane Instance found") } request := &mpi.CreateConnectionRequest{ diff --git a/internal/command/command_service_test.go b/internal/command/command_service_test.go index fb7564065d..ad8c6f790c 100644 --- a/internal/command/command_service_test.go +++ b/internal/command/command_service_test.go @@ -127,6 +127,23 @@ func TestCommandService_UpdateDataPlaneStatusSubscribeError(t *testing.T) { } } +func TestCommandService_CreateConnection(t *testing.T) { + ctx := context.Background() + commandServiceClient := &v1fakes.FakeCommandServiceClient{} + + commandService := NewCommandService( + ctx, + commandServiceClient, + types.AgentConfig(), + make(chan *mpi.ManagementPlaneRequest), + ) + + // connection created when no nginx instance found + resource := protos.GetHostResource() + _, err := commandService.CreateConnection(ctx, resource) + require.NoError(t, err) +} + func TestCommandService_UpdateDataPlaneHealth(t *testing.T) { ctx := context.Background() commandServiceClient := &v1fakes.FakeCommandServiceClient{} diff --git a/test/docker/nginxless-entrypoint.sh b/test/docker/nginxless-entrypoint.sh new file mode 100644 index 0000000000..b971d9108d --- /dev/null +++ b/test/docker/nginxless-entrypoint.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -euxo pipefail + +handle_term() +{ + echo "received TERM signal" + echo "stopping nginx-agent ..." + kill -TERM "${agent_pid}" 2>/dev/null +} + +trap 'handle_term' TERM + +cat /etc/nginx-agent/nginx-agent.conf; + +# start nginx-agent, pass args +echo "starting nginx-agent ..." +nginx-agent "$@" & + +agent_pid=$! + +if [ $? != 0 ]; then + echo "couldn't start the agent, please check the log file" + exit 1 +fi + +wait_term() +{ + wait ${agent_pid} +} + +wait_term + +echo "nginx-agent process has stopped, exiting." diff --git a/test/helpers/test_containers_utils.go b/test/helpers/test_containers_utils.go index 25c633e86d..4070d6f633 100644 --- a/test/helpers/test_containers_utils.go +++ b/test/helpers/test_containers_utils.go @@ -158,6 +158,75 @@ func StartAgentlessContainer( return container } +func StartNginxLessContainer( + ctx context.Context, + tb testing.TB, + containerNetwork *testcontainers.DockerNetwork, + parameters *Parameters, +) testcontainers.Container { + tb.Helper() + + packageName := Env(tb, "PACKAGE_NAME") + packageRepo := Env(tb, "PACKAGES_REPO") + baseImage := Env(tb, "BASE_IMAGE") + buildTarget := Env(tb, "BUILD_TARGET") + osRelease := Env(tb, "OS_RELEASE") + osVersion := Env(tb, "OS_VERSION") + dockerfilePath := Env(tb, "DOCKERFILE_PATH") + tag := Env(tb, "TAG") + imagePath := Env(tb, "IMAGE_PATH") + containerRegistry := Env(tb, "CONTAINER_NGINX_IMAGE_REGISTRY") + + req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ + Context: "../../", + Dockerfile: dockerfilePath, + KeepImage: false, + PrintBuildLog: true, + BuildArgs: map[string]*string{ + "PACKAGE_NAME": ToPtr(packageName), + "PACKAGES_REPO": ToPtr(packageRepo), + "BASE_IMAGE": ToPtr(baseImage), + "OS_RELEASE": ToPtr(osRelease), + "OS_VERSION": ToPtr(osVersion), + "ENTRY_POINT": ToPtr("./test/docker/nginxless-entrypoint.sh"), + "CONTAINER_NGINX_IMAGE_REGISTRY": ToPtr(containerRegistry), + "IMAGE_PATH": ToPtr(imagePath), + "TAG": ToPtr(tag), + }, + BuildOptionsModifier: func(buildOptions *types.ImageBuildOptions) { + buildOptions.Target = buildTarget + }, + }, + ExposedPorts: []string{"9094/tcp"}, + WaitingFor: wait.ForLog(parameters.LogMessage), + Networks: []string{ + containerNetwork.Name, + }, + NetworkAliases: map[string][]string{ + containerNetwork.Name: { + "agent", + }, + }, + Files: []testcontainers.ContainerFile{ + { + HostFilePath: parameters.NginxAgentConfigPath, + ContainerFilePath: "/etc/nginx-agent/nginx-agent.conf", + FileMode: configFilePermissions, + }, + }, + } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + + require.NoError(tb, err) + + return container +} + func StartMockManagementPlaneGrpcContainer( ctx context.Context, tb testing.TB, diff --git a/test/integration/grpc_management_plane_api_test.go b/test/integration/grpc_management_plane_api_test.go index 76825ca120..6952ffa507 100644 --- a/test/integration/grpc_management_plane_api_test.go +++ b/test/integration/grpc_management_plane_api_test.go @@ -64,121 +64,148 @@ type ( } ) -func setupConnectionTest(tb testing.TB, expectNoErrorsInLogs bool) func(tb testing.TB) { +func setupConnectionTest(tb testing.TB, expectNoErrorsInLogs, nginxless bool) func(tb testing.TB) { tb.Helper() ctx := context.Background() if os.Getenv("TEST_ENV") == "Container" { - tb.Log("Running tests in a container environment") + setupContainerEnvironment(ctx, tb, nginxless) + } else { + setupLocalEnvironment(tb) + } - containerNetwork, err := network.New( - ctx, - network.WithAttachable(), - ) - require.NoError(tb, err) - tb.Cleanup(func() { - networkErr := containerNetwork.Remove(ctx) - tb.Logf("Error removing container network: %v", networkErr) - require.NoError(tb, networkErr) - }) - - mockManagementPlaneGrpcContainer = helpers.StartMockManagementPlaneGrpcContainer( - ctx, - tb, - containerNetwork, - ) + return func(tb testing.TB) { + tb.Helper() - mockManagementPlaneGrpcAddress = "managementPlane:9092" - tb.Logf("Mock management gRPC server running on %s", mockManagementPlaneGrpcAddress) + if os.Getenv("TEST_ENV") == "Container" { + helpers.LogAndTerminateContainers( + ctx, + tb, + mockManagementPlaneGrpcContainer, + container, + expectNoErrorsInLogs, + ) + } + } +} - ipAddress, err := mockManagementPlaneGrpcContainer.Host(ctx) - require.NoError(tb, err) - ports, err := mockManagementPlaneGrpcContainer.Ports(ctx) - require.NoError(tb, err) +// setupContainerEnvironment sets up the container environment for testing. +func setupContainerEnvironment(ctx context.Context, tb testing.TB, nginxless bool) { + tb.Helper() + tb.Log("Running tests in a container environment") - mockManagementPlaneAPIAddress = net.JoinHostPort(ipAddress, ports["9093/tcp"][0].HostPort) - tb.Logf("Mock management API server running on %s", mockManagementPlaneAPIAddress) + containerNetwork := createContainerNetwork(ctx, tb) + setupMockManagementPlaneGrpc(ctx, tb, containerNetwork) - nginxConfPath := "../config/nginx/nginx.conf" - if os.Getenv("IMAGE_PATH") == "/nginx-plus/agent" { - nginxConfPath = "../config/nginx/nginx-plus.conf" - } + params := &helpers.Parameters{ + NginxAgentConfigPath: "../config/agent/nginx-config-with-grpc-client.conf", + LogMessage: "Agent connected", + } + switch nginxless { + case true: + container = helpers.StartNginxLessContainer(ctx, tb, containerNetwork, params) + case false: + setupNginxContainer(ctx, tb, containerNetwork, params) + } +} - params := &helpers.Parameters{ - NginxConfigPath: nginxConfPath, - NginxAgentConfigPath: "../config/agent/nginx-config-with-grpc-client.conf", - LogMessage: "Agent connected", - } +// createContainerNetwork creates and configures a container network. +func createContainerNetwork(ctx context.Context, tb testing.TB) *testcontainers.DockerNetwork { + tb.Helper() + containerNetwork, err := network.New(ctx, network.WithAttachable()) + require.NoError(tb, err) + tb.Cleanup(func() { + networkErr := containerNetwork.Remove(ctx) + tb.Logf("Error removing container network: %v", networkErr) + require.NoError(tb, networkErr) + }) - container = helpers.StartContainer( - ctx, - tb, - containerNetwork, - params, - ) - } else { - requestChan := make(chan *mpi.ManagementPlaneRequest) - server := mockGrpc.NewCommandService(requestChan, os.TempDir()) + return containerNetwork +} - go func(tb testing.TB) { - tb.Helper() +// setupMockManagementPlaneGrpc initializes the mock management plane gRPC container. +func setupMockManagementPlaneGrpc(ctx context.Context, tb testing.TB, containerNetwork *testcontainers.DockerNetwork) { + tb.Helper() + mockManagementPlaneGrpcContainer = helpers.StartMockManagementPlaneGrpcContainer(ctx, tb, containerNetwork) + mockManagementPlaneGrpcAddress = "managementPlane:9092" + tb.Logf("Mock management gRPC server running on %s", mockManagementPlaneGrpcAddress) - listener, err := net.Listen("tcp", "localhost:0") - assert.NoError(tb, err) + ipAddress, err := mockManagementPlaneGrpcContainer.Host(ctx) + require.NoError(tb, err) + ports, err := mockManagementPlaneGrpcContainer.Ports(ctx) + require.NoError(tb, err) - mockManagementPlaneAPIAddress = listener.Addr().String() + mockManagementPlaneAPIAddress = net.JoinHostPort(ipAddress, ports["9093/tcp"][0].HostPort) + tb.Logf("Mock management API server running on %s", mockManagementPlaneAPIAddress) +} + +// setupNginxContainer configures and starts the NGINX container. +func setupNginxContainer( + ctx context.Context, + tb testing.TB, + containerNetwork *testcontainers.DockerNetwork, + params *helpers.Parameters, +) { + tb.Helper() + nginxConfPath := "../config/nginx/nginx.conf" + if os.Getenv("IMAGE_PATH") == "/nginx-plus/agent" { + nginxConfPath = "../config/nginx/nginx-plus.conf" + } + params.NginxConfigPath = nginxConfPath - server.StartServer(listener) - }(tb) + container = helpers.StartContainer(ctx, tb, containerNetwork, params) +} - go func(tb testing.TB) { - tb.Helper() +// setupLocalEnvironment configures the local testing environment. +func setupLocalEnvironment(tb testing.TB) { + tb.Helper() + tb.Log("Running tests on local machine") - listener, err := net.Listen("tcp", "localhost:0") - assert.NoError(tb, err) - var opts []grpc.ServerOption + requestChan := make(chan *mpi.ManagementPlaneRequest) + server := mockGrpc.NewCommandService(requestChan, os.TempDir()) - grpcServer := grpc.NewServer(opts...) - mpi.RegisterCommandServiceServer(grpcServer, server) - err = grpcServer.Serve(listener) - assert.NoError(tb, err) + go func(tb testing.TB) { + tb.Helper() - mockManagementPlaneGrpcAddress = listener.Addr().String() - }(tb) + listener, err := net.Listen("tcp", "localhost:0") + assert.NoError(tb, err) - tb.Log("Running tests on local machine") - } + mockManagementPlaneAPIAddress = listener.Addr().String() - return func(tb testing.TB) { + server.StartServer(listener) + }(tb) + + go func(tb testing.TB) { tb.Helper() - if os.Getenv("TEST_ENV") == "Container" { - helpers.LogAndTerminateContainers( - ctx, - tb, - mockManagementPlaneGrpcContainer, - container, - expectNoErrorsInLogs, - ) - } - } + listener, err := net.Listen("tcp", "localhost:0") + assert.NoError(tb, err) + var opts []grpc.ServerOption + + grpcServer := grpc.NewServer(opts...) + mpi.RegisterCommandServiceServer(grpcServer, server) + err = grpcServer.Serve(listener) + assert.NoError(tb, err) + + mockManagementPlaneGrpcAddress = listener.Addr().String() + }(tb) } // Verify that the agent sends a connection request and an update data plane status request func TestGrpc_StartUp(t *testing.T) { - teardownTest := setupConnectionTest(t, true) + teardownTest := setupConnectionTest(t, true, false) defer teardownTest(t) - verifyConnection(t) + verifyConnection(t, 2) assert.False(t, t.Failed()) verifyUpdateDataPlaneHealth(t) } func TestGrpc_ConfigUpload(t *testing.T) { - teardownTest := setupConnectionTest(t, true) + teardownTest := setupConnectionTest(t, true, false) defer teardownTest(t) - nginxInstanceID := verifyConnection(t) + nginxInstanceID := verifyConnection(t, 2) assert.False(t, t.Failed()) request := fmt.Sprintf(`{ @@ -211,7 +238,7 @@ func TestGrpc_ConfigUpload(t *testing.T) { func TestGrpc_ConfigApply(t *testing.T) { ctx := context.Background() - teardownTest := setupConnectionTest(t, false) + teardownTest := setupConnectionTest(t, false, false) defer teardownTest(t) instanceType := "OSS" @@ -219,7 +246,7 @@ func TestGrpc_ConfigApply(t *testing.T) { instanceType = "PLUS" } - nginxInstanceID := verifyConnection(t) + nginxInstanceID := verifyConnection(t, 2) responses := getManagementPlaneResponses(t, 1) assert.Equal(t, mpi.CommandResponse_COMMAND_STATUS_OK, responses[0].GetCommandResponse().GetStatus()) @@ -321,10 +348,10 @@ func TestGrpc_ConfigApply(t *testing.T) { func TestGrpc_FileWatcher(t *testing.T) { ctx := context.Background() - teardownTest := setupConnectionTest(t, true) + teardownTest := setupConnectionTest(t, true, false) defer teardownTest(t) - verifyConnection(t) + verifyConnection(t, 2) assert.False(t, t.Failed()) err := container.CopyFileToContainer( @@ -345,10 +372,10 @@ func TestGrpc_FileWatcher(t *testing.T) { } func TestGrpc_DataplaneHealthRequest(t *testing.T) { - teardownTest := setupConnectionTest(t, true) + teardownTest := setupConnectionTest(t, true, false) defer teardownTest(t) - verifyConnection(t) + verifyConnection(t, 2) responses := getManagementPlaneResponses(t, 1) assert.Equal(t, mpi.CommandResponse_COMMAND_STATUS_OK, responses[0].GetCommandResponse().GetStatus()) @@ -493,7 +520,7 @@ func clearManagementPlaneResponses(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode()) } -func verifyConnection(t *testing.T) string { +func verifyConnection(t *testing.T, instancesLength int) string { t.Helper() client := resty.New() @@ -522,7 +549,7 @@ func verifyConnection(t *testing.T) string { assert.NotNil(t, resource.GetResourceId()) assert.NotNil(t, resource.GetContainerInfo().GetContainerId()) - assert.Len(t, resource.GetInstances(), 2) + assert.Len(t, resource.GetInstances(), instancesLength) var nginxInstanceID string diff --git a/test/integration/nginx_less_mpi_connection_test.go b/test/integration/nginx_less_mpi_connection_test.go new file mode 100644 index 0000000000..7d0870c4ee --- /dev/null +++ b/test/integration/nginx_less_mpi_connection_test.go @@ -0,0 +1,21 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package integration + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Verify that the agent sends a connection request to Management Plane even when Nginx is not present +func TestNginxLessGrpc_Connection(t *testing.T) { + teardownTest := setupConnectionTest(t, true, true) + defer teardownTest(t) + + verifyConnection(t, 1) + assert.False(t, t.Failed()) +}