diff --git a/controllers/container_network_tunnel_proxy_controller.go b/controllers/container_network_tunnel_proxy_controller.go index baefe4bb..c0df1f6b 100644 --- a/controllers/container_network_tunnel_proxy_controller.go +++ b/controllers/container_network_tunnel_proxy_controller.go @@ -98,6 +98,10 @@ var ( type ContainerNetworkTunnelProxyReconcilerConfig struct { ProcessExecutor process.Executor // Mandatory + // Directory that owns the lifetime of generated tunnel proxy image build contexts. + // Defaults to the DCP temporary directory. + BuildContextDir string + // The factory function to create a TunnelControlClient used to control the proxy pair. // Normal execution uses "real" gRPC client, tests use a stub since most tests do not run real tunnels. // Mandatory. @@ -132,6 +136,9 @@ func NewContainerNetworkTunnelProxyReconciler( if config.ProcessExecutor == nil { panic("ContainerNetworkTunnelProxyReconcilerConfig.ProcessExecutor must not be nil") } + if config.BuildContextDir == "" { + config.BuildContextDir = usvc_io.DcpTempDir() + } if config.MakeTunnelControlClient == nil { panic("ContainerNetworkTunnelProxyReconcilerConfig.TunnelControlClientFactory must not be nil") } @@ -1204,7 +1211,7 @@ func (r *ContainerNetworkTunnelProxyReconciler) ensureTunnelProxyPhysicalContain return fmt.Errorf("get PhysicalContainerImage %q: %w", imageName.String(), getErr) } - imagePlan, prepareErr := dcptun.PrepareClientProxyImageBuild(ctx) + imagePlan, prepareErr := dcptun.PrepareClientProxyImageBuild(ctx, r.config.BuildContextDir) if prepareErr != nil { return prepareErr } diff --git a/internal/dcptun/image_builder.go b/internal/dcptun/image_builder.go index 1d1f01ef..5b3ef826 100644 --- a/internal/dcptun/image_builder.go +++ b/internal/dcptun/image_builder.go @@ -52,7 +52,11 @@ type ClientProxyImageBuildPlan struct { // PrepareClientProxyImageBuild creates the build input for the shared tunnel proxy // PhysicalContainerImage. -func PrepareClientProxyImageBuild(ctx context.Context) (ClientProxyImageBuildPlan, error) { +func PrepareClientProxyImageBuild(ctx context.Context, buildContextDir string) (ClientProxyImageBuildPlan, error) { + if buildContextDir == "" { + return ClientProxyImageBuildPlan{}, fmt.Errorf("build context directory is required") + } + dcpTunClientPath, clientPathErr := dcptunClientBinaryPath() if clientPathErr != nil { return ClientProxyImageBuildPlan{}, fmt.Errorf("failed to get path to dcptun client binary: %w", clientPathErr) @@ -69,7 +73,7 @@ func PrepareClientProxyImageBuild(ctx context.Context) (ClientProxyImageBuildPla if digestErr != nil { return ClientProxyImageBuildPlan{}, digestErr } - buildContextArchive, contextErr := setupImageBuildContextArchive(ctx, dcpTunClientPath, dockerfileContent) + buildContextArchive, contextErr := setupImageBuildContextArchive(ctx, buildContextDir, dcpTunClientPath, dockerfileContent) if contextErr != nil { return ClientProxyImageBuildPlan{}, fmt.Errorf("failed to create build context archive: %w", contextErr) } @@ -99,6 +103,7 @@ func clientProxyImageName(clientBinaryHash string) string { func setupImageBuildContextArchive( ctx context.Context, + buildContextDir string, dcpTunClientPath string, dockerfileContent string, ) (*containers.ContainerBuildContextArchive, error) { @@ -110,9 +115,9 @@ func setupImageBuildContextArchive( if randomSuffixErr != nil { return nil, fmt.Errorf("create random build context archive suffix: %w", randomSuffixErr) } - // The archive lives in the DCP session directory, whose lifetime owns its cleanup. - archiveFile, openArchiveErr := usvc_io.CreateNewTempFile( - fmt.Sprintf("dcptun-build-context-%s.tar", randomSuffix), + // The caller-provided directory owns the archive lifetime. + archiveFile, openArchiveErr := usvc_io.CreateNewFile( + filepath.Join(buildContextDir, fmt.Sprintf("dcptun-build-context-%s.tar", randomSuffix)), osutil.PermissionOnlyOwnerReadWrite, ) if openArchiveErr != nil { diff --git a/internal/dcptun/image_builder_test.go b/internal/dcptun/image_builder_test.go index 5d40a5b1..bc1bf967 100644 --- a/internal/dcptun/image_builder_test.go +++ b/internal/dcptun/image_builder_test.go @@ -10,6 +10,7 @@ import ( "context" "io" "os" + "path/filepath" "strings" "testing" @@ -24,8 +25,9 @@ func TestPrepareClientProxyImageBuild(t *testing.T) { t.Parallel() dcppaths.EnableTestPathProbing() + buildContextDir := t.TempDir() - plan, prepareErr := dcptun.PrepareClientProxyImageBuild(context.Background()) + plan, prepareErr := dcptun.PrepareClientProxyImageBuild(context.Background(), buildContextDir) require.NoError(t, prepareErr) const expectedTagPrefix = "dcptun_developer_ms:" @@ -34,6 +36,7 @@ func TestPrepareClientProxyImageBuild(t *testing.T) { require.NotNil(t, plan.BuildContextArchive) require.NotEmpty(t, plan.BuildContextDigest) require.NotEmpty(t, plan.BuildContextArchive.Source) + require.Equal(t, buildContextDir, filepath.Dir(plan.BuildContextArchive.Source)) require.NotEmpty(t, plan.BuildContextArchive.SHA256) require.NotEqual(t, "sha256:"+plan.BuildContextArchive.SHA256, plan.BuildContextDigest) require.Empty(t, plan.BuildContextArchive.RawContents) @@ -42,9 +45,10 @@ func TestPrepareClientProxyImageBuild(t *testing.T) { require.NoError(t, os.Remove(plan.BuildContextArchive.Source)) }) - secondPlan, secondPrepareErr := dcptun.PrepareClientProxyImageBuild(context.Background()) + secondPlan, secondPrepareErr := dcptun.PrepareClientProxyImageBuild(context.Background(), buildContextDir) require.NoError(t, secondPrepareErr) require.Equal(t, plan.BuildContextDigest, secondPlan.BuildContextDigest) + require.Equal(t, buildContextDir, filepath.Dir(secondPlan.BuildContextArchive.Source)) t.Cleanup(func() { require.NoError(t, os.Remove(secondPlan.BuildContextArchive.Source)) }) @@ -77,6 +81,6 @@ func TestPrepareClientProxyImageBuildHonorsCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - _, prepareErr := dcptun.PrepareClientProxyImageBuild(ctx) + _, prepareErr := dcptun.PrepareClientProxyImageBuild(ctx, t.TempDir()) require.ErrorIs(t, prepareErr, context.Canceled) } diff --git a/test/integration/advanced_test_env.go b/test/integration/advanced_test_env.go index f88ef99d..7033e6b0 100644 --- a/test/integration/advanced_test_env.go +++ b/test/integration/advanced_test_env.go @@ -347,6 +347,7 @@ func StartAdvancedTestEnvironmentWithOptions( if inclCtrl&ContainerNetworkTunnelProxyController != 0 { tprOpts := controllers.ContainerNetworkTunnelProxyReconcilerConfig{ ProcessExecutor: pe, + BuildContextDir: sessionFolder, MakeTunnelControlClient: dcptunproto.NewTunnelControlClient, MaxTunnelPreparationAttempts: 2, } diff --git a/test/integration/container_network_tunnel_proxy_test.go b/test/integration/container_network_tunnel_proxy_test.go index 5ea196be..12d5c2b7 100644 --- a/test/integration/container_network_tunnel_proxy_test.go +++ b/test/integration/container_network_tunnel_proxy_test.go @@ -185,26 +185,14 @@ func TestTunnelProxyRunningStatus(t *testing.T) { dcppaths.EnableTestPathProbing() const testName = "test-tunnel-proxy-running-status" - buildContextDir, mkdirErr := os.MkdirTemp("", "dcp-tunnel-") - require.NoError(t, mkdirErr) - t.Cleanup(func() { - require.NoError(t, os.RemoveAll(buildContextDir)) - }) - originalDcpTempDir := usvc_io.DcpTempDir - usvc_io.DcpTempDir = func() string { - return buildContextDir - } - t.Cleanup(func() { - usvc_io.DcpTempDir = originalDcpTempDir - }) - includedControllers := ServiceController | NetworkController | ContainerNetworkTunnelProxyController serverInfo, teInfo, startupErr := StartTestEnvironment(ctx, includedControllers, t.Name(), t.TempDir()) require.NoError(t, startupErr, "Failed to start the API server") defer shutdownTestEnvironment(serverInfo, cancel) testContainerOrchestrator, ok := serverInfo.ContainerOrchestrator.(*ctrl_testutil.TestContainerOrchestrator) require.True(t, ok) - imagePlan, imagePlanErr := dcptun.PrepareClientProxyImageBuild(ctx) + buildContextDir := teInfo.SessionFolder + imagePlan, imagePlanErr := dcptun.PrepareClientProxyImageBuild(ctx, buildContextDir) require.NoError(t, imagePlanErr) require.NoError(t, os.Remove(imagePlan.BuildContextArchive.Source)) releaseImageBuild := testContainerOrchestrator.BlockBuildImage(imagePlan.Image) @@ -316,7 +304,7 @@ func TestTunnelProxyRunningStatus(t *testing.T) { require.NoError(t, globErr) require.Equal(t, []string{physicalImages.Items[0].Spec.Image.Build.ContextArchive.Source}, buildContextFiles) t.Cleanup(func() { - require.NoError(t, os.Remove(physicalImages.Items[0].Spec.Image.Build.ContextArchive.Source)) + require.NoFileExists(t, physicalImages.Items[0].Spec.Image.Build.ContextArchive.Source) }) require.Equal(t, apiv2.PhysicalContainerImagePhaseReady, physicalImages.Items[0].Status.Phase) require.Equal(t, 1, testContainerOrchestrator.BuildImageCallCount(updatedTunnelProxy.Status.ClientProxyContainerImage)) @@ -509,7 +497,7 @@ func TestTunnelProxyCleanup(t *testing.T) { require.Empty(t, physicalImage.Spec.Image.Build.ContextArchive.RawContents) require.FileExists(t, physicalImage.Spec.Image.Build.ContextArchive.Source) t.Cleanup(func() { - require.NoError(t, os.Remove(physicalImage.Spec.Image.Build.ContextArchive.Source)) + require.NoFileExists(t, physicalImage.Spec.Image.Build.ContextArchive.Source) }) t.Logf("Deleting ContainerNetworkTunnelProxy object '%s'", tunnelProxy.ObjectMeta.Name) diff --git a/test/integration/standard_test_env.go b/test/integration/standard_test_env.go index 7e78fb82..1bccdb2d 100644 --- a/test/integration/standard_test_env.go +++ b/test/integration/standard_test_env.go @@ -42,6 +42,7 @@ type TestEnvironmentInfo struct { StateStore *statestore.Store ResourceLeaseOwner process.ProcessHandle Log logr.Logger + SessionFolder string } type TestEnvironmentOptions struct { @@ -377,6 +378,7 @@ func StartTestEnvironmentWithOptions( tcc = ctrl_testutil.NewTestTunnelControlClient() tprOpts := controllers.ContainerNetworkTunnelProxyReconcilerConfig{ ProcessExecutor: pex, + BuildContextDir: sessionFolder, MakeTunnelControlClient: func(_ grpc.ClientConnInterface) dcptunproto.TunnelControlClient { return tcc }, MaxTunnelPreparationAttempts: 2, } @@ -414,6 +416,7 @@ func StartTestEnvironmentWithOptions( StateStore: stateStore, ResourceLeaseOwner: leaseOwner, Log: log, + SessionFolder: sessionFolder, } return serverInfo, teInfo, nil }