Skip to content
Merged
Show file tree
Hide file tree
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
56 changes: 56 additions & 0 deletions .github/workflows/pr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ jobs:

printf '%s\n' "$packages" | xargs go test -race -short

- name: test Compose UID expectation matrix
shell: bash
run: |
set -euo pipefail
go test ./e2e/tests/up-docker-compose -list '^TestExpectedUIDMapping$' \
| grep -Fx 'TestExpectedUIDMapping'
go test -race -short -count=1 -v ./e2e/tests/up-docker-compose \
-run '^TestExpectedUIDMapping$'

can-read-secret:
name: Can Read Secret
runs-on: ubuntu-latest
Expand Down Expand Up @@ -1030,6 +1039,53 @@ jobs:
private-key: ${{ secrets.DEVSY_GITHUB_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}

- name: test Compose UID mapping as existing non-root runner
if: runner.os == 'Linux' && matrix.label == 'up-docker-compose'
shell: bash
working-directory: ./e2e
env:
TEST_TIMEOUT: ${{ matrix.test-timeout || '1500s' }}
run: |
set -euo pipefail
id
test "$(id -u)" = 1001
test "$(id -g)" = 1001
getent passwd "$(id -u)"
getent group "$(id -g)"
test -d "$HOME" && test -w "$HOME"
test "$(stat -c %u "$HOME")" = "$(id -u)"
test -d "$RUNNER_TEMP" && test -w "$RUNNER_TEMP"
docker info
docker compose version
printf 'checkout=%s\n' "$(git rev-parse HEAD)"
sha256sum ./e2e.test ./bin/devsy-linux-amd64
./e2e.test -test.v -ginkgo.v -test.timeout "${TEST_TIMEOUT}" \
-ginkgo.fail-on-empty -ginkgo.flake-attempts=1 \
--ginkgo.label-filter=up-docker-compose \
--ginkgo.focus='implements updateRemoteUserUID with root container user$' \
--ginkgo.json-report="$RUNNER_TEMP/compose-uid-nonroot.json"
python3 - "$RUNNER_TEMP/compose-uid-nonroot.json" <<'PY'
import json
import sys

with open(sys.argv[1], encoding="utf-8") as report:
suites = json.load(report)
assert isinstance(suites, list) and len(suites) == 1, "unexpected suite report shape"
suite = suites[0]
assert suite["SuiteConfig"]["DryRun"] is False, "dry-run is not execution"
assert suite["SuiteSucceeded"] is True, "suite failed"
selected = [
spec for spec in suite["SpecReports"]
if spec.get("LeafNodeType") == "It" and spec.get("State") != "skipped"
]
assert len(selected) == 1, f"expected one executed leaf, found {len(selected)}"
spec = selected[0]
assert spec["LeafNodeText"] == "implements updateRemoteUserUID with root container user", "wrong UID spec"
assert spec["State"] == "passed", "UID spec did not pass"
assert spec["NumAttempts"] == 1, "unexpected retries"
print("one www-data UID spec passed in one attempt")
PY

- name: run test
if: matrix.requires-secret == false || needs.can-read-secret.outputs.secret-set == 'true'
shell: bash
Expand Down
54 changes: 36 additions & 18 deletions e2e/tests/up-docker-compose/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
Expand Down Expand Up @@ -281,30 +282,47 @@ func verifyContainerUser(
To(gomega.Equal(expectedUser), fmt.Sprintf("remote container user should be %s", expectedUser))
}

type userIDs struct {
uid, gid int
}

func expectedUIDMapping(goos string, host, defaults userIDs) (int, int) {
if goos == "linux" && host.uid != 0 {
return host.uid, host.gid
}
return defaults.uid, defaults.gid
}

func verifyUIDMapping(
containerUID, containerGID, hostUID, hostGID, defaultUID, defaultGID int,
username string,
) {
ginkgo.By(
fmt.Sprintf("container UID mapping: %s=%d, expected=%d", username, containerUID, hostUID),
expectedUID, expectedGID := expectedUIDMapping(
runtime.GOOS,
userIDs{uid: hostUID, gid: hostGID},
userIDs{uid: defaultUID, gid: defaultGID},
)
ginkgo.By(
fmt.Sprintf("container GID mapping: %s=%d, expected=%d", username, containerGID, hostGID),
ginkgo.By(fmt.Sprintf(
"container UID/GID mapping: user=%s os=%s host=%d:%d default=%d:%d actual=%d:%d expected=%d:%d",
username,
runtime.GOOS,
hostUID,
hostGID,
defaultUID,
defaultGID,
containerUID,
containerGID,
expectedUID,
expectedGID,
))
gomega.Expect(containerUID).To(
gomega.Equal(expectedUID),
fmt.Sprintf("%s UID should match computed platform expectation", username),
)
gomega.Expect(containerGID).To(
gomega.Equal(expectedGID),
fmt.Sprintf("%s GID should match computed platform expectation", username),
)

if hostUID == 0 {
ginkgo.By("running as root user on host")
gomega.Expect(containerUID).
To(gomega.Equal(defaultUID), fmt.Sprintf("%s user UID should remain %d when host is root", username, defaultUID))
gomega.Expect(containerGID).
To(gomega.Equal(defaultGID), fmt.Sprintf("%s user GID should remain %d when host is root", username, defaultGID))
} else {
ginkgo.By("running as non-root user on host")
gomega.Expect(containerUID).
To(gomega.Equal(hostUID), fmt.Sprintf("%s user UID should match host user UID", username))
gomega.Expect(containerGID).
To(gomega.Equal(hostGID), fmt.Sprintf("%s user GID should match host user GID", username))
}
}

func verifyHostFileAccess(filePath, expectedContent string) {
Expand Down
55 changes: 55 additions & 0 deletions e2e/tests/up-docker-compose/helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//go:build linux || darwin || unix

package up

import "testing"

func TestExpectedUIDMapping(t *testing.T) {
const (
linux = "linux"
darwin = "darwin"
)
tests := []struct {
name string
goos string
hostUID, hostGID int
defaultUID, defaultGID int
wantUID, wantGID int
}{
{"linux root www-data", linux, 0, 0, 33, 33, 33, 33},
{"linux root vscode", linux, 0, 0, 1001, 1001, 1001, 1001},
{"linux root nonzero GID www-data", linux, 0, 2002, 33, 33, 33, 33},
{"linux root nonzero GID vscode", linux, 0, 2002, 1001, 1001, 1001, 1001},
{"linux 501 www-data", linux, 501, 20, 33, 33, 501, 20},
{"linux 501 vscode", linux, 501, 20, 1001, 1001, 501, 20},
{"linux distinct IDs www-data", linux, 2000, 2002, 33, 33, 2000, 2002},
{"linux distinct IDs vscode", linux, 2000, 2002, 1001, 1001, 2000, 2002},
{"linux non-root zero GID www-data", linux, 2000, 0, 33, 33, 2000, 0},
{"linux non-root zero GID vscode", linux, 2000, 0, 1001, 1001, 2000, 0},
{"darwin user www-data", darwin, 501, 20, 33, 33, 33, 33},
{"darwin user vscode", darwin, 501, 20, 1001, 1001, 1001, 1001},
{"darwin root www-data", darwin, 0, 0, 33, 33, 33, 33},
{"darwin root vscode", darwin, 0, 0, 1001, 1001, 1001, 1001},
{"freebsd user www-data", "freebsd", 501, 20, 33, 33, 33, 33},
{"freebsd user vscode", "freebsd", 501, 20, 1001, 1001, 1001, 1001},
{"linux matching defaults www-data", linux, 33, 33, 33, 33, 33, 33},
{"linux matching defaults vscode", linux, 1001, 1001, 1001, 1001, 1001, 1001},
{"linux UID mismatch only www-data", linux, 2000, 33, 33, 33, 2000, 33},
{"linux UID mismatch only vscode", linux, 2000, 1001, 1001, 1001, 2000, 1001},
{"linux GID mismatch only www-data", linux, 33, 2002, 33, 33, 33, 2002},
{"linux GID mismatch only vscode", linux, 1001, 2002, 1001, 1001, 1001, 2002},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
uid, gid := expectedUIDMapping(
tc.goos,
userIDs{uid: tc.hostUID, gid: tc.hostGID},
userIDs{uid: tc.defaultUID, gid: tc.defaultGID},
)
if uid != tc.wantUID || gid != tc.wantGID {
t.Fatalf("got %d:%d want %d:%d", uid, gid, tc.wantUID, tc.wantGID)
}
})
}
}
Loading