From ca1ddbf93ce62d708492ec28945bca04ec88bafa Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Thu, 10 Oct 2024 14:13:44 +0200 Subject: [PATCH 1/5] Use SetPermissions instead of UpdatePermissions when setting folder permissions based on top-level ones --- bundle/permissions/workspace_root.go | 43 +++++++- bundle/permissions/workspace_root_test.go | 121 +++++++++++++++++++++- 2 files changed, 160 insertions(+), 4 deletions(-) diff --git a/bundle/permissions/workspace_root.go b/bundle/permissions/workspace_root.go index 93a90ed9cf..ee5bba466f 100644 --- a/bundle/permissions/workspace_root.go +++ b/bundle/permissions/workspace_root.go @@ -3,6 +3,7 @@ package permissions import ( "context" "fmt" + "strings" "github.com/databricks/cli/bundle" "github.com/databricks/cli/libs/diag" @@ -52,16 +53,54 @@ func giveAccessForWorkspaceRoot(ctx context.Context, b *bundle.Bundle) error { } w := b.WorkspaceClient().Workspace - obj, err := w.GetStatusByPath(ctx, b.Config.Workspace.RootPath) + err := setPermissions(ctx, w, b.Config.Workspace.RootPath, permissions) if err != nil { return err } - _, err = w.UpdatePermissions(ctx, workspace.WorkspaceObjectPermissionsRequest{ + if !strings.HasPrefix(b.Config.Workspace.ArtifactPath, b.Config.Workspace.RootPath) { + err = setPermissions(ctx, w, b.Config.Workspace.ArtifactPath, permissions) + if err != nil { + return err + } + } + + if !strings.HasPrefix(b.Config.Workspace.FilePath, b.Config.Workspace.RootPath) { + err = setPermissions(ctx, w, b.Config.Workspace.FilePath, permissions) + if err != nil { + return err + } + } + + if !strings.HasPrefix(b.Config.Workspace.StatePath, b.Config.Workspace.RootPath) { + err = setPermissions(ctx, w, b.Config.Workspace.StatePath, permissions) + if err != nil { + return err + } + } + + if !strings.HasPrefix(b.Config.Workspace.ResourcePath, b.Config.Workspace.RootPath) { + err = setPermissions(ctx, w, b.Config.Workspace.ResourcePath, permissions) + if err != nil { + return err + } + } + + return err +} + +func setPermissions(ctx context.Context, w workspace.WorkspaceInterface, path string, permissions []workspace.WorkspaceObjectAccessControlRequest) error { + obj, err := w.GetStatusByPath(ctx, path) + if err != nil { + return err + } + + _, err = w.SetPermissions(ctx, workspace.WorkspaceObjectPermissionsRequest{ WorkspaceObjectId: fmt.Sprint(obj.ObjectId), WorkspaceObjectType: "directories", AccessControlList: permissions, }) + return err } diff --git a/bundle/permissions/workspace_root_test.go b/bundle/permissions/workspace_root_test.go index 6b37b2c411..c48704a638 100644 --- a/bundle/permissions/workspace_root_test.go +++ b/bundle/permissions/workspace_root_test.go @@ -21,7 +21,11 @@ func TestApplyWorkspaceRootPermissions(t *testing.T) { b := &bundle.Bundle{ Config: config.Root{ Workspace: config.Workspace{ - RootPath: "/Users/foo@bar.com", + RootPath: "/Users/foo@bar.com", + ArtifactPath: "/Users/foo@bar.com/artifacts", + FilePath: "/Users/foo@bar.com/files", + StatePath: "/Users/foo@bar.com/state", + ResourcePath: "/Users/foo@bar.com/resources", }, Permissions: []resources.Permission{ {Level: CAN_MANAGE, UserName: "TestUser"}, @@ -59,7 +63,7 @@ func TestApplyWorkspaceRootPermissions(t *testing.T) { workspaceApi.EXPECT().GetStatusByPath(mock.Anything, "/Users/foo@bar.com").Return(&workspace.ObjectInfo{ ObjectId: 1234, }, nil) - workspaceApi.EXPECT().UpdatePermissions(mock.Anything, workspace.WorkspaceObjectPermissionsRequest{ + workspaceApi.EXPECT().SetPermissions(mock.Anything, workspace.WorkspaceObjectPermissionsRequest{ AccessControlList: []workspace.WorkspaceObjectAccessControlRequest{ {UserName: "TestUser", PermissionLevel: "CAN_MANAGE"}, {GroupName: "TestGroup", PermissionLevel: "CAN_READ"}, @@ -72,3 +76,116 @@ func TestApplyWorkspaceRootPermissions(t *testing.T) { diags := bundle.Apply(context.Background(), b, bundle.Seq(ValidateSharedRootPermissions(), ApplyWorkspaceRootPermissions())) require.Empty(t, diags) } + +func TestApplyWorkspaceRootPermissionsForAllPaths(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Workspace: config.Workspace{ + RootPath: "/Some/Root/Path", + ArtifactPath: "/Users/foo@bar.com/artifacts", + FilePath: "/Users/foo@bar.com/files", + StatePath: "/Users/foo@bar.com/state", + ResourcePath: "/Users/foo@bar.com/resources", + }, + Permissions: []resources.Permission{ + {Level: CAN_MANAGE, UserName: "TestUser"}, + {Level: CAN_VIEW, GroupName: "TestGroup"}, + {Level: CAN_RUN, ServicePrincipalName: "TestServicePrincipal"}, + }, + Resources: config.Resources{ + Jobs: map[string]*resources.Job{ + "job_1": {JobSettings: &jobs.JobSettings{Name: "job_1"}}, + "job_2": {JobSettings: &jobs.JobSettings{Name: "job_2"}}, + }, + Pipelines: map[string]*resources.Pipeline{ + "pipeline_1": {PipelineSpec: &pipelines.PipelineSpec{}}, + "pipeline_2": {PipelineSpec: &pipelines.PipelineSpec{}}, + }, + Models: map[string]*resources.MlflowModel{ + "model_1": {Model: &ml.Model{}}, + "model_2": {Model: &ml.Model{}}, + }, + Experiments: map[string]*resources.MlflowExperiment{ + "experiment_1": {Experiment: &ml.Experiment{}}, + "experiment_2": {Experiment: &ml.Experiment{}}, + }, + ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{ + "endpoint_1": {CreateServingEndpoint: &serving.CreateServingEndpoint{}}, + "endpoint_2": {CreateServingEndpoint: &serving.CreateServingEndpoint{}}, + }, + }, + }, + } + + m := mocks.NewMockWorkspaceClient(t) + b.SetWorkpaceClient(m.WorkspaceClient) + workspaceApi := m.GetMockWorkspaceAPI() + workspaceApi.EXPECT().GetStatusByPath(mock.Anything, "/Some/Root/Path").Return(&workspace.ObjectInfo{ + ObjectId: 1, + }, nil) + workspaceApi.EXPECT().GetStatusByPath(mock.Anything, "/Users/foo@bar.com/artifacts").Return(&workspace.ObjectInfo{ + ObjectId: 2, + }, nil) + workspaceApi.EXPECT().GetStatusByPath(mock.Anything, "/Users/foo@bar.com/files").Return(&workspace.ObjectInfo{ + ObjectId: 3, + }, nil) + workspaceApi.EXPECT().GetStatusByPath(mock.Anything, "/Users/foo@bar.com/state").Return(&workspace.ObjectInfo{ + ObjectId: 4, + }, nil) + workspaceApi.EXPECT().GetStatusByPath(mock.Anything, "/Users/foo@bar.com/resources").Return(&workspace.ObjectInfo{ + ObjectId: 5, + }, nil) + + workspaceApi.EXPECT().SetPermissions(mock.Anything, workspace.WorkspaceObjectPermissionsRequest{ + AccessControlList: []workspace.WorkspaceObjectAccessControlRequest{ + {UserName: "TestUser", PermissionLevel: "CAN_MANAGE"}, + {GroupName: "TestGroup", PermissionLevel: "CAN_READ"}, + {ServicePrincipalName: "TestServicePrincipal", PermissionLevel: "CAN_RUN"}, + }, + WorkspaceObjectId: "1", + WorkspaceObjectType: "directories", + }).Return(nil, nil) + + workspaceApi.EXPECT().SetPermissions(mock.Anything, workspace.WorkspaceObjectPermissionsRequest{ + AccessControlList: []workspace.WorkspaceObjectAccessControlRequest{ + {UserName: "TestUser", PermissionLevel: "CAN_MANAGE"}, + {GroupName: "TestGroup", PermissionLevel: "CAN_READ"}, + {ServicePrincipalName: "TestServicePrincipal", PermissionLevel: "CAN_RUN"}, + }, + WorkspaceObjectId: "2", + WorkspaceObjectType: "directories", + }).Return(nil, nil) + + workspaceApi.EXPECT().SetPermissions(mock.Anything, workspace.WorkspaceObjectPermissionsRequest{ + AccessControlList: []workspace.WorkspaceObjectAccessControlRequest{ + {UserName: "TestUser", PermissionLevel: "CAN_MANAGE"}, + {GroupName: "TestGroup", PermissionLevel: "CAN_READ"}, + {ServicePrincipalName: "TestServicePrincipal", PermissionLevel: "CAN_RUN"}, + }, + WorkspaceObjectId: "3", + WorkspaceObjectType: "directories", + }).Return(nil, nil) + + workspaceApi.EXPECT().SetPermissions(mock.Anything, workspace.WorkspaceObjectPermissionsRequest{ + AccessControlList: []workspace.WorkspaceObjectAccessControlRequest{ + {UserName: "TestUser", PermissionLevel: "CAN_MANAGE"}, + {GroupName: "TestGroup", PermissionLevel: "CAN_READ"}, + {ServicePrincipalName: "TestServicePrincipal", PermissionLevel: "CAN_RUN"}, + }, + WorkspaceObjectId: "4", + WorkspaceObjectType: "directories", + }).Return(nil, nil) + + workspaceApi.EXPECT().SetPermissions(mock.Anything, workspace.WorkspaceObjectPermissionsRequest{ + AccessControlList: []workspace.WorkspaceObjectAccessControlRequest{ + {UserName: "TestUser", PermissionLevel: "CAN_MANAGE"}, + {GroupName: "TestGroup", PermissionLevel: "CAN_READ"}, + {ServicePrincipalName: "TestServicePrincipal", PermissionLevel: "CAN_RUN"}, + }, + WorkspaceObjectId: "5", + WorkspaceObjectType: "directories", + }).Return(nil, nil) + + diags := bundle.Apply(context.Background(), b, ApplyWorkspaceRootPermissions()) + require.NoError(t, diags.Error()) +} From 99fbf33ef1f51a002999d6bb214564ce5a51c1cf Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Thu, 10 Oct 2024 15:24:52 +0200 Subject: [PATCH 2/5] fixes --- bundle/permissions/workspace_root.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bundle/permissions/workspace_root.go b/bundle/permissions/workspace_root.go index ee5bba466f..a176141a38 100644 --- a/bundle/permissions/workspace_root.go +++ b/bundle/permissions/workspace_root.go @@ -58,28 +58,34 @@ func giveAccessForWorkspaceRoot(ctx context.Context, b *bundle.Bundle) error { return err } - if !strings.HasPrefix(b.Config.Workspace.ArtifactPath, b.Config.Workspace.RootPath) { + // Adding backslash to the root path + rootPath := b.Config.Workspace.RootPath + if rootPath[len(rootPath)-1] != '/' { + rootPath += "/" + } + + if !strings.HasPrefix(b.Config.Workspace.ArtifactPath, rootPath) { err = setPermissions(ctx, w, b.Config.Workspace.ArtifactPath, permissions) if err != nil { return err } } - if !strings.HasPrefix(b.Config.Workspace.FilePath, b.Config.Workspace.RootPath) { + if !strings.HasPrefix(b.Config.Workspace.FilePath, rootPath) { err = setPermissions(ctx, w, b.Config.Workspace.FilePath, permissions) if err != nil { return err } } - if !strings.HasPrefix(b.Config.Workspace.StatePath, b.Config.Workspace.RootPath) { + if !strings.HasPrefix(b.Config.Workspace.StatePath, rootPath) { err = setPermissions(ctx, w, b.Config.Workspace.StatePath, permissions) if err != nil { return err } } - if !strings.HasPrefix(b.Config.Workspace.ResourcePath, b.Config.Workspace.RootPath) { + if !strings.HasPrefix(b.Config.Workspace.ResourcePath, rootPath) { err = setPermissions(ctx, w, b.Config.Workspace.ResourcePath, permissions) if err != nil { return err From a51c5731fed13f023c17c38a3efa893bc5f89f17 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Tue, 22 Oct 2024 16:28:32 +0200 Subject: [PATCH 3/5] fixes --- bundle/permissions/workspace_root.go | 54 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/bundle/permissions/workspace_root.go b/bundle/permissions/workspace_root.go index a176141a38..bd8b0ca5a5 100644 --- a/bundle/permissions/workspace_root.go +++ b/bundle/permissions/workspace_root.go @@ -6,8 +6,10 @@ import ( "strings" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/libraries" "github.com/databricks/cli/libs/diag" "github.com/databricks/databricks-sdk-go/service/workspace" + "golang.org/x/sync/errgroup" ) type workspaceRootPermissions struct { @@ -53,46 +55,44 @@ func giveAccessForWorkspaceRoot(ctx context.Context, b *bundle.Bundle) error { } w := b.WorkspaceClient().Workspace - err := setPermissions(ctx, w, b.Config.Workspace.RootPath, permissions) - if err != nil { - return err + rootPath := b.Config.Workspace.RootPath + paths := []string{} + if !libraries.IsVolumesPath(rootPath) { + paths = append(paths, rootPath) } - // Adding backslash to the root path - rootPath := b.Config.Workspace.RootPath - if rootPath[len(rootPath)-1] != '/' { + if !strings.HasSuffix(rootPath, "/") { rootPath += "/" } - if !strings.HasPrefix(b.Config.Workspace.ArtifactPath, rootPath) { - err = setPermissions(ctx, w, b.Config.Workspace.ArtifactPath, permissions) - if err != nil { - return err - } + if !strings.HasPrefix(b.Config.Workspace.ArtifactPath, rootPath) && + !libraries.IsVolumesPath(b.Config.Workspace.ArtifactPath) { + paths = append(paths, b.Config.Workspace.ArtifactPath) } - if !strings.HasPrefix(b.Config.Workspace.FilePath, rootPath) { - err = setPermissions(ctx, w, b.Config.Workspace.FilePath, permissions) - if err != nil { - return err - } + if !strings.HasPrefix(b.Config.Workspace.FilePath, rootPath) && + !libraries.IsVolumesPath(b.Config.Workspace.FilePath) { + paths = append(paths, b.Config.Workspace.FilePath) } - if !strings.HasPrefix(b.Config.Workspace.StatePath, rootPath) { - err = setPermissions(ctx, w, b.Config.Workspace.StatePath, permissions) - if err != nil { - return err - } + if !strings.HasPrefix(b.Config.Workspace.StatePath, rootPath) && + !libraries.IsVolumesPath(b.Config.Workspace.StatePath) { + paths = append(paths, b.Config.Workspace.StatePath) } - if !strings.HasPrefix(b.Config.Workspace.ResourcePath, rootPath) { - err = setPermissions(ctx, w, b.Config.Workspace.ResourcePath, permissions) - if err != nil { - return err - } + if !strings.HasPrefix(b.Config.Workspace.ResourcePath, rootPath) && + !libraries.IsVolumesPath(b.Config.Workspace.ResourcePath) { + paths = append(paths, b.Config.Workspace.ResourcePath) } - return err + g, ctx := errgroup.WithContext(ctx) + for _, p := range paths { + g.Go(func() error { + return setPermissions(ctx, w, p, permissions) + }) + } + + return g.Wait() } func setPermissions(ctx context.Context, w workspace.WorkspaceInterface, path string, permissions []workspace.WorkspaceObjectAccessControlRequest) error { From 16f02c0e506fbbe492dc1bb73485daf61fbb978c Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Thu, 24 Oct 2024 18:39:21 +0200 Subject: [PATCH 4/5] reuse --- bundle/config/validate/folder_permissions.go | 34 ++--------------- bundle/libraries/path.go | 1 + bundle/paths/paths.go | 39 ++++++++++++++++++++ bundle/permissions/workspace_root.go | 35 ++---------------- 4 files changed, 47 insertions(+), 62 deletions(-) create mode 100644 bundle/paths/paths.go diff --git a/bundle/config/validate/folder_permissions.go b/bundle/config/validate/folder_permissions.go index a376bd776c..56d36cfed3 100644 --- a/bundle/config/validate/folder_permissions.go +++ b/bundle/config/validate/folder_permissions.go @@ -4,10 +4,9 @@ import ( "context" "fmt" "path" - "strings" "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/libraries" + "github.com/databricks/cli/bundle/paths" "github.com/databricks/cli/bundle/permissions" "github.com/databricks/cli/libs/diag" "github.com/databricks/databricks-sdk-go/apierr" @@ -24,37 +23,12 @@ func (f *folderPermissions) Apply(ctx context.Context, b bundle.ReadOnlyBundle) return nil } - rootPath := b.Config().Workspace.RootPath - paths := []string{} - if !libraries.IsVolumesPath(rootPath) && !libraries.IsWorkspaceSharedPath(rootPath) { - paths = append(paths, rootPath) - } - - if !strings.HasSuffix(rootPath, "/") { - rootPath += "/" - } - - for _, p := range []string{ - b.Config().Workspace.ArtifactPath, - b.Config().Workspace.FilePath, - b.Config().Workspace.StatePath, - b.Config().Workspace.ResourcePath, - } { - if libraries.IsWorkspaceSharedPath(p) || libraries.IsVolumesPath(p) { - continue - } - - if strings.HasPrefix(p, rootPath) { - continue - } - - paths = append(paths, p) - } + bundlePaths := paths.CollectUniquePaths(b.Config().Workspace) var diags diag.Diagnostics g, ctx := errgroup.WithContext(ctx) - results := make([]diag.Diagnostics, len(paths)) - for i, p := range paths { + results := make([]diag.Diagnostics, len(bundlePaths)) + for i, p := range bundlePaths { g.Go(func() error { results[i] = checkFolderPermission(ctx, b, p) return nil diff --git a/bundle/libraries/path.go b/bundle/libraries/path.go index 3bad40face..418d9ca73c 100644 --- a/bundle/libraries/path.go +++ b/bundle/libraries/path.go @@ -38,6 +38,7 @@ func IsWorkspaceLibrary(library *compute.Library) bool { } // IsVolumesPath returns true if the specified path indicates that +// it should be interpreted as a Databricks Volumes path. func IsVolumesPath(path string) bool { return strings.HasPrefix(path, "/Volumes/") } diff --git a/bundle/paths/paths.go b/bundle/paths/paths.go new file mode 100644 index 0000000000..c092543e62 --- /dev/null +++ b/bundle/paths/paths.go @@ -0,0 +1,39 @@ +package paths + +import ( + "strings" + + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/libraries" +) + +func CollectUniquePaths(workspace config.Workspace) []string { + rootPath := workspace.RootPath + paths := []string{} + if !libraries.IsVolumesPath(rootPath) && !libraries.IsWorkspaceSharedPath(rootPath) { + paths = append(paths, rootPath) + } + + if !strings.HasSuffix(rootPath, "/") { + rootPath += "/" + } + + for _, p := range []string{ + workspace.ArtifactPath, + workspace.FilePath, + workspace.StatePath, + workspace.ResourcePath, + } { + if libraries.IsWorkspaceSharedPath(p) || libraries.IsVolumesPath(p) { + continue + } + + if strings.HasPrefix(p, rootPath) { + continue + } + + paths = append(paths, p) + } + + return paths +} diff --git a/bundle/permissions/workspace_root.go b/bundle/permissions/workspace_root.go index bd8b0ca5a5..d52c658cd4 100644 --- a/bundle/permissions/workspace_root.go +++ b/bundle/permissions/workspace_root.go @@ -3,10 +3,9 @@ package permissions import ( "context" "fmt" - "strings" "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/libraries" + "github.com/databricks/cli/bundle/paths" "github.com/databricks/cli/libs/diag" "github.com/databricks/databricks-sdk-go/service/workspace" "golang.org/x/sync/errgroup" @@ -55,38 +54,10 @@ func giveAccessForWorkspaceRoot(ctx context.Context, b *bundle.Bundle) error { } w := b.WorkspaceClient().Workspace - rootPath := b.Config.Workspace.RootPath - paths := []string{} - if !libraries.IsVolumesPath(rootPath) { - paths = append(paths, rootPath) - } - - if !strings.HasSuffix(rootPath, "/") { - rootPath += "/" - } - - if !strings.HasPrefix(b.Config.Workspace.ArtifactPath, rootPath) && - !libraries.IsVolumesPath(b.Config.Workspace.ArtifactPath) { - paths = append(paths, b.Config.Workspace.ArtifactPath) - } - - if !strings.HasPrefix(b.Config.Workspace.FilePath, rootPath) && - !libraries.IsVolumesPath(b.Config.Workspace.FilePath) { - paths = append(paths, b.Config.Workspace.FilePath) - } - - if !strings.HasPrefix(b.Config.Workspace.StatePath, rootPath) && - !libraries.IsVolumesPath(b.Config.Workspace.StatePath) { - paths = append(paths, b.Config.Workspace.StatePath) - } - - if !strings.HasPrefix(b.Config.Workspace.ResourcePath, rootPath) && - !libraries.IsVolumesPath(b.Config.Workspace.ResourcePath) { - paths = append(paths, b.Config.Workspace.ResourcePath) - } + bundlePaths := paths.CollectUniquePaths(b.Config.Workspace) g, ctx := errgroup.WithContext(ctx) - for _, p := range paths { + for _, p := range bundlePaths { g.Go(func() error { return setPermissions(ctx, w, p, permissions) }) From 6659e573c4c25cffd796802236b8434a60de2f61 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Tue, 29 Oct 2024 11:50:05 +0100 Subject: [PATCH 5/5] changed name --- bundle/config/validate/folder_permissions.go | 2 +- bundle/paths/paths.go | 2 +- bundle/permissions/workspace_root.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bundle/config/validate/folder_permissions.go b/bundle/config/validate/folder_permissions.go index 56d36cfed3..88502ec8f9 100644 --- a/bundle/config/validate/folder_permissions.go +++ b/bundle/config/validate/folder_permissions.go @@ -23,7 +23,7 @@ func (f *folderPermissions) Apply(ctx context.Context, b bundle.ReadOnlyBundle) return nil } - bundlePaths := paths.CollectUniquePaths(b.Config().Workspace) + bundlePaths := paths.CollectUniqueWorkspacePathPrefixes(b.Config().Workspace) var diags diag.Diagnostics g, ctx := errgroup.WithContext(ctx) diff --git a/bundle/paths/paths.go b/bundle/paths/paths.go index c092543e62..50b75a6cda 100644 --- a/bundle/paths/paths.go +++ b/bundle/paths/paths.go @@ -7,7 +7,7 @@ import ( "github.com/databricks/cli/bundle/libraries" ) -func CollectUniquePaths(workspace config.Workspace) []string { +func CollectUniqueWorkspacePathPrefixes(workspace config.Workspace) []string { rootPath := workspace.RootPath paths := []string{} if !libraries.IsVolumesPath(rootPath) && !libraries.IsWorkspaceSharedPath(rootPath) { diff --git a/bundle/permissions/workspace_root.go b/bundle/permissions/workspace_root.go index d52c658cd4..4ab8198bb4 100644 --- a/bundle/permissions/workspace_root.go +++ b/bundle/permissions/workspace_root.go @@ -54,7 +54,7 @@ func giveAccessForWorkspaceRoot(ctx context.Context, b *bundle.Bundle) error { } w := b.WorkspaceClient().Workspace - bundlePaths := paths.CollectUniquePaths(b.Config.Workspace) + bundlePaths := paths.CollectUniqueWorkspacePathPrefixes(b.Config.Workspace) g, ctx := errgroup.WithContext(ctx) for _, p := range bundlePaths {