From b3b9f983892675075d8f6a7af510943733094969 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 19 Aug 2026 23:23:10 +0000 Subject: [PATCH] fix(agent): stop excluding .devsy-internal from streamed workspace mount The Kubernetes dockerless fallback generates .devsy-internal build artifacts on the host before the workspace is streamed into the pod. StreamWorkspace and StreamMount both appended config.BuildArtifactExcludes() to their tar excludes, stripping .devsy-internal from that stream. With no bind mount for Kubernetes, this was the only transport into the pod, so prepareBuildDirectory could never find the build context and dockerless build failed before starting. The exclude was introduced in 8e32743af (#615) to fix build-artifact leakage into LocalDockerDelivery's docker-volume seeding (workspace_seed.go), which has its own independent tar/exclude logic and is unaffected by this change. Fixes #1108 --- e2e/tests/build/build.go | 43 +++++++++ pkg/agent/tunnelserver/tunnelserver.go | 4 +- pkg/agent/tunnelserver/tunnelserver_test.go | 99 +++++++++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) diff --git a/e2e/tests/build/build.go b/e2e/tests/build/build.go index c4e993f4c..8c48082fc 100644 --- a/e2e/tests/build/build.go +++ b/e2e/tests/build/build.go @@ -383,6 +383,49 @@ var _ = ginkgo.Describe("devsy build test suite", ginkgo.Label("build"), ginkgo. }, ) }) + + ginkgo.It( + "kubernetes dockerless build cleans up .devsy-internal after streaming", + ginkgo.SpecTimeout(framework.TimeoutShort()), + func(ctx context.Context) { + if runtime.GOOS == osWindows { + ginkgo.Skip("skipping on windows") + } + + f := framework.NewDefaultFramework(initialDir + "/bin") + tempDir, err := framework.CopyToTempDir("tests/build/testdata/kubernetes") + framework.ExpectNoError(err) + ginkgo.DeferCleanup(framework.CleanupTempDir, initialDir, tempDir) + + _ = f.DevsyProviderDelete(ctx, "kubernetes") + err = f.DevsyProviderAdd(ctx, "kubernetes") + framework.ExpectNoError(err) + err = f.DevsyProviderUse(ctx, "kubernetes", "-o", "KUBERNETES_NAMESPACE=devsy") + framework.ExpectNoError(err) + + ginkgo.DeferCleanup(f.DevsyWorkspaceDelete, tempDir) + + err = f.DevsyUp(ctx, tempDir) + framework.ExpectNoError(err) + + out, err := f.DevsySSH(ctx, tempDir, "echo -n $MY_TEST") + framework.ExpectNoError(err) + framework.ExpectEqual(out, "test456") + + workspaceArtifact := "/workspaces/" + filepath.Base(tempDir) + + "/.devcontainer/" + config.DevsyContextFeatureFolder + _, err = f.DevsySSH(ctx, tempDir, "test -e "+workspaceArtifact) + framework.ExpectError(err) + + hostArtifact := filepath.Join( + tempDir, + ".devcontainer", + config.DevsyContextFeatureFolder, + ) + _, statErr := os.Stat(hostArtifact) + framework.ExpectEqual(os.IsNotExist(statErr), true) + }, + ) }) func validateKubernetesDeploymentWithoutDocker( diff --git a/pkg/agent/tunnelserver/tunnelserver.go b/pkg/agent/tunnelserver/tunnelserver.go index 9c29d15d4..7386280bd 100644 --- a/pkg/agent/tunnelserver/tunnelserver.go +++ b/pkg/agent/tunnelserver/tunnelserver.go @@ -482,8 +482,6 @@ func (t *tunnelServer) StreamWorkspace( } } - excludes = append(excludes, config.BuildArtifactExcludes()...) - buf := bufio.NewWriterSize(NewStreamWriter(stream), 10*1024) err = extract.WriteTarExclude(buf, t.workspace.Source.LocalFolder, false, excludes) if err != nil { @@ -516,7 +514,7 @@ func (t *tunnelServer) StreamMount( return fmt.Errorf("mount %s is not allowed to download", message.Mount) } - excludes := append(t.workspaceIgnoreExcludes(), config.BuildArtifactExcludes()...) + excludes := t.workspaceIgnoreExcludes() buf := bufio.NewWriterSize(NewStreamWriter(stream), 10*1024) err := extract.WriteTarExclude(buf, mount.Source, false, excludes) diff --git a/pkg/agent/tunnelserver/tunnelserver_test.go b/pkg/agent/tunnelserver/tunnelserver_test.go index 12fd047db..4ed62ebb1 100644 --- a/pkg/agent/tunnelserver/tunnelserver_test.go +++ b/pkg/agent/tunnelserver/tunnelserver_test.go @@ -74,6 +74,105 @@ func TestStreamSnapshotVolumes_TarsMountTargets(t *testing.T) { require.True(t, found, "expected tar entry %q not found", wantName) } +type fakeStreamServer struct { + tunnel.Tunnel_StreamWorkspaceServer + chunks [][]byte +} + +func (f *fakeStreamServer) Send(c *tunnel.Chunk) error { + f.chunks = append(f.chunks, c.Content) + return nil +} + +func (f *fakeStreamServer) Context() context.Context { return context.Background() } + +func tarEntryNames(t *testing.T, chunks [][]byte) []string { + t.Helper() + var buf bytes.Buffer + for _, c := range chunks { + buf.Write(c) + } + var names []string + tr := tar.NewReader(&buf) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + require.NoError(t, err) + names = append(names, hdr.Name) + } + return names +} + +func TestStreamWorkspace_IncludesDevsyInternalBuildArtifacts(t *testing.T) { + dir := t.TempDir() + internalDir := filepath.Join(dir, config.DevsyContextFeatureFolder) + require.NoError(t, os.MkdirAll(internalDir, 0o750)) + require.NoError( + t, + os.WriteFile( + filepath.Join(internalDir, "Dockerfile-without-features"), + []byte("FROM scratch"), + 0o600, + ), + ) + + srv := &tunnelServer{ + workspace: &provider2.Workspace{Source: provider2.WorkspaceSource{LocalFolder: dir}}, + } + + fake := &fakeStreamServer{} + require.NoError(t, srv.StreamWorkspace(&tunnel.Empty{}, fake)) + + names := tarEntryNames(t, fake.chunks) + require.Contains( + t, + names, + filepath.ToSlash(filepath.Join( + config.DevsyContextFeatureFolder, + "Dockerfile-without-features", + )), + ) +} + +func TestStreamMount_IncludesDevsyInternalBuildArtifacts(t *testing.T) { + dir := t.TempDir() + mountSource := filepath.Join(dir, "mount") + internalDir := filepath.Join(mountSource, config.DevsyContextFeatureFolder) + require.NoError(t, os.MkdirAll(internalDir, 0o750)) + require.NoError( + t, + os.WriteFile( + filepath.Join(internalDir, "Dockerfile-without-features"), + []byte("FROM scratch"), + 0o600, + ), + ) + + srv := &tunnelServer{ + mounts: []*config.Mount{ + {Source: mountSource, Target: "/workspaces/e2e"}, + }, + } + + fake := &fakeStreamServer{} + require.NoError( + t, + srv.StreamMount(&tunnel.StreamMountRequest{Mount: srv.mounts[0].String()}, fake), + ) + + names := tarEntryNames(t, fake.chunks) + require.Contains( + t, + names, + filepath.ToSlash(filepath.Join( + config.DevsyContextFeatureFolder, + "Dockerfile-without-features", + )), + ) +} + func TestRunWithResult_CancelBeforeResult(t *testing.T) { srv := New()