Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 81 additions & 49 deletions pkg/devcontainer/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading