diff --git a/pkg/devcontainer/setup/setup.go b/pkg/devcontainer/setup/setup.go index 65634e5f66..89d9024d2e 100644 --- a/pkg/devcontainer/setup/setup.go +++ b/pkg/devcontainer/setup/setup.go @@ -540,70 +540,102 @@ func linkRootHome(setupInfo *config.Result) error { func chownWorkspace(setupInfo *config.Result, recursive bool) error { workspaceFolder := setupInfo.SubstitutionContext.ContainerWorkspaceFolder - // Compose services are not guaranteed a workspaceMount - if _, err := os.Lstat(workspaceFolder); err != nil { - if os.IsNotExist(err) { - log.Debugf("skip chown: workspace folder does not exist: %s", workspaceFolder) - return nil - } - return fmt.Errorf("stat workspace %s: %w", workspaceFolder, err) + present, err := workspaceFolderPresent(workspaceFolder) + if err != nil || !present { + return err } - user := config.GetRemoteUser(setupInfo) - // Scope the marker to a workspace so a restored image does not suppress - // ownership setup for a different workspace; an unset workspace ID can't - // be scoped, so skip the marker gate entirely and chown every run. workspaceID := os.Getenv(pkgconfig.EnvWorkspaceID) - if workspaceID != "" { - exists, err := markerExists("chownWorkspace", workspaceID) - if err != nil { - return err - } else if exists { - return nil - } + latched, err := workspaceChownLatched(workspaceID) + if err != nil || latched { + return err } - workspaceRoot := filepath.Dir(workspaceFolder) - if workspaceRoot != "/" { - log.Debugf("chown workspace: user=%s, workspaceRoot=%s", user, workspaceRoot) - if err := copy2.Chown(workspaceRoot, user); err != nil && !copy2.DeniedByFilesystem(err) { - return fmt.Errorf("chown %s: %w", workspaceRoot, err) - } else if err != nil { - // A non-root container (e.g. an OpenShift restricted-SCC pod) does - // not own workspaceRoot and cannot chown it; the workspace folder - // itself may still get a usable owner below. - log.Debugf("chown workspace: %s kept its owner: %v", workspaceRoot, err) - } + user := config.GetRemoteUser(setupInfo) + if err := chownWorkspaceRoot(user, workspaceFolder); err != nil { + return err } if recursive { - log.Debugf( - "chown workspace recursively: user=%s, workspaceFolder=%s", - user, - workspaceFolder, - ) - err := copy2.ChownR(workspaceFolder, user) - var failures copy2.ChownFailures - switch { - case err == nil: - case errors.As(err, &failures) && failures.AllDenied(): - // Read-only shared files, such as virtiofs pack files, can refuse chown. - // A future container for this workspace may not share the same read-only mount. - log.Warnf("chown workspace: %d entries kept their owner: %v", len(failures), err) - return nil - default: - return fmt.Errorf("chown %s recursively: %w", workspaceFolder, err) + persist, err := chownWorkspaceTree(user, workspaceFolder) + if err != nil || !persist { + return err } } - if workspaceID != "" { - if err := writeMarker("chownWorkspace", workspaceID); err != nil { - return err - } + return recordWorkspaceChown(workspaceID) +} + +// Compose services are not guaranteed a workspaceMount. +func workspaceFolderPresent(workspaceFolder string) (bool, error) { + _, err := os.Lstat(workspaceFolder) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + log.Debugf("skip chown: workspace folder does not exist: %s", workspaceFolder) + return false, nil + } + return false, fmt.Errorf("stat workspace %s: %w", workspaceFolder, err) +} + +// Scope the marker to a workspace so a restored image does not suppress +// ownership setup for a different workspace; an unset workspace ID can't +// be scoped, so skip the marker gate entirely and chown every run. +func workspaceChownLatched(workspaceID string) (bool, error) { + if workspaceID == "" { + return false, nil + } + return markerExists("chownWorkspace", workspaceID) +} + +func chownWorkspaceRoot(user, workspaceFolder string) error { + workspaceRoot := filepath.Dir(workspaceFolder) + if workspaceRoot == "/" { + return nil + } + log.Debugf("chown workspace: user=%s, workspaceRoot=%s", user, workspaceRoot) + if err := copy2.Chown(workspaceRoot, user); err != nil && !copy2.DeniedByFilesystem(err) { + return fmt.Errorf("chown %s: %w", workspaceRoot, err) + } else if err != nil { + // A non-root container (e.g. an OpenShift restricted-SCC pod) does + // not own workspaceRoot and cannot chown it; the workspace folder + // itself may still get a usable owner below. + log.Debugf("chown workspace: %s kept its owner: %v", workspaceRoot, err) } return nil } +// chownWorkspaceTree reports false when every entry kept its owner, so the +// marker is not written and a future container for this workspace retries. +func chownWorkspaceTree(user, workspaceFolder string) (bool, error) { + log.Debugf( + "chown workspace recursively: user=%s, workspaceFolder=%s", + user, + workspaceFolder, + ) + err := copy2.ChownR(workspaceFolder, user) + var failures copy2.ChownFailures + switch { + case err == nil: + return true, nil + case errors.As(err, &failures) && failures.AllDenied(): + // Read-only shared files, such as virtiofs pack files, can refuse chown. + // A future container for this workspace may not share the same read-only mount. + log.Warnf("chown workspace: %d entries kept their owner: %v", len(failures), err) + return false, nil + default: + return false, fmt.Errorf("chown %s recursively: %w", workspaceFolder, err) + } +} + +func recordWorkspaceChown(workspaceID string) error { + if workspaceID == "" { + return nil + } + return writeMarker("chownWorkspace", workspaceID) +} + func patchEtcProfile() error { exists, err := markerFileExists("patchEtcProfile", "") if err != nil {