diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index bf0e18e9..821a762e 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -43,14 +43,7 @@ jobs:
- name: Test
run: |
- go test -c ./internals/daemon
- PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./daemon.test -check.v -check.f ^execSuite\.TestUserGroup$
- PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./daemon.test -check.v -check.f ^execSuite\.TestUserIDGroupID$
- PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./daemon.test -check.v -check.f ^filesSuite\.TestWriteUserGroupReal$
- PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./daemon.test -check.v -check.f ^filesSuite\.TestMakeDirsUserGroupReal$
- go test -c ./internals/osutil
- PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./osutil.test -check.v -check.f ^mkdirSuite\.TestMakeParentsChmodAndChown$
-
+ PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H go test -count=1 -tags=roottest -run=TestWithRoot ./...
go test -c ./internals/overlord/servstate/
PEBBLE_TEST_USER=runner PEBBLE_TEST_GROUP=runner sudo -E -H ./servstate.test -check.v -check.f ^S.TestUserGroup$
diff --git a/internals/daemon/api_exec_root_test.go b/internals/daemon/api_exec_root_test.go
new file mode 100644
index 00000000..a1584325
--- /dev/null
+++ b/internals/daemon/api_exec_root_test.go
@@ -0,0 +1,179 @@
+//go:build roottest
+
+// Copyright (c) 2024 Canonical Ltd
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package daemon
+
+import (
+ "bytes"
+ "fmt"
+ "log"
+ "os"
+ "os/user"
+ "regexp"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/canonical/pebble/client"
+ "github.com/canonical/pebble/internals/reaper"
+)
+
+var rootTestDaemon *Daemon
+var rootTestPebbleClient *client.Client
+
+func TestMain(m *testing.M) {
+ err := reaper.Start()
+ if err != nil {
+ fmt.Printf("cannot start reaper: %v", err)
+ os.Exit(1)
+ }
+ tmpDir, err := os.MkdirTemp("", "pebble")
+ if err != nil {
+ fmt.Printf("cannot create temporary directory: %v", err)
+ os.Exit(1)
+ }
+ socketPath := tmpDir + ".pebble.socket"
+ rootTestDaemon, err := New(&Options{
+ Dir: tmpDir,
+ SocketPath: socketPath,
+ })
+ if err != nil {
+ fmt.Printf("cannot create daemon: %v", err)
+ os.Exit(1)
+ }
+ err = rootTestDaemon.Init()
+ if err != nil {
+ fmt.Printf("cannot init daemon: %v", err)
+ os.Exit(1)
+ }
+ rootTestDaemon.Start()
+ rootTestPebbleClient, err = client.New(&client.Config{Socket: socketPath})
+ if err != nil {
+ fmt.Printf("cannot create client: %v", err)
+ os.Exit(1)
+ }
+
+ exitCode := m.Run()
+
+ err = rootTestDaemon.Stop(nil)
+ if err != nil {
+ fmt.Printf("cannot stop daemon: %v", err)
+ os.Exit(1)
+ }
+ err = reaper.Stop()
+ if err != nil {
+ fmt.Printf("cannot stop reaper: %v", err)
+ os.Exit(1)
+ }
+ err = os.RemoveAll(tmpDir)
+ if err != nil {
+ log.Fatalf("cannot remove temporary directory: %v", err)
+ }
+
+ os.Exit(exitCode)
+}
+
+func TestWithRootUserGroup(t *testing.T) {
+ if os.Getuid() != 0 {
+ t.Skip("requires running as root")
+ }
+ username := os.Getenv("PEBBLE_TEST_USER")
+ group := os.Getenv("PEBBLE_TEST_GROUP")
+ if username == "" || group == "" {
+ t.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP")
+ }
+ stdout, stderr := pebbleExec(t, "", &client.ExecOptions{
+ Command: []string{"/bin/sh", "-c", "id -n -u && id -n -g"},
+ User: username,
+ Group: group,
+ })
+ expectedStdout := username + "\n" + group + "\n"
+ if stdout != expectedStdout {
+ t.Fatalf("pebble exec stdout error, expected: %v, got %v", expectedStdout, stdout)
+ }
+ if stderr != "" {
+ t.Fatalf("pebble exec stderr is not empty: %v", stderr)
+ }
+
+ _, err := rootTestPebbleClient.Exec(&client.ExecOptions{
+ Command: []string{"pwd"},
+ Environment: map[string]string{"HOME": "/non/existent"},
+ User: username,
+ Group: group,
+ })
+ // c.Assert(err, ErrorMatches, `.*home directory.*does not exist`)
+ if matched, _ := regexp.MatchString(`.*home directory.*does not exist`, err.Error()); !matched {
+ t.Errorf("Error message doesn't match, expected: %v, got: %v", `.*home directory.*does not exist`, err.Error())
+ }
+}
+
+func TestWithRootUserIDGroupID(t *testing.T) {
+ if os.Getuid() != 0 {
+ t.Skip("requires running as root")
+ }
+ username := os.Getenv("PEBBLE_TEST_USER")
+ group := os.Getenv("PEBBLE_TEST_GROUP")
+ if username == "" || group == "" {
+ t.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP")
+ }
+ u, err := user.Lookup(username)
+ if err != nil {
+ t.Fatalf("cannot look up username: %v", err)
+ }
+ g, err := user.LookupGroup(group)
+ if err != nil {
+ t.Fatalf("cannot look up group: %v", err)
+ }
+ uid, err := strconv.Atoi(u.Uid)
+ if err != nil {
+ t.Fatalf("cannot convert uid to int: %v", err)
+ }
+ gid, err := strconv.Atoi(g.Gid)
+ if err != nil {
+ t.Fatalf("cannot convert gid to int: %v", err)
+ }
+ stdout, stderr := pebbleExec(t, "", &client.ExecOptions{
+ Command: []string{"/bin/sh", "-c", "id -n -u && id -n -g"},
+ UserID: &uid,
+ GroupID: &gid,
+ })
+ expectedStdout := username + "\n" + group + "\n"
+ if stdout != expectedStdout {
+ t.Fatalf("pebble exec stdout error, expected: %v, got %v", expectedStdout, stdout)
+ }
+ if stderr != "" {
+ t.Fatalf("pebble exec stderr is not empty: %v", stderr)
+ }
+}
+
+func pebbleExec(t *testing.T, stdin string, opts *client.ExecOptions) (stdout, stderr string) {
+ t.Helper()
+
+ outBuf := &bytes.Buffer{}
+ errBuf := &bytes.Buffer{}
+ opts.Stdin = strings.NewReader(stdin)
+ opts.Stdout = outBuf
+ opts.Stderr = errBuf
+ process, err := rootTestPebbleClient.Exec(opts)
+ if err != nil {
+ t.Fatalf("pebble exec failed: %v", err)
+ }
+
+ if waitErr := process.Wait(); waitErr != nil {
+ t.Fatalf("pebble exec process wait error: %v", waitErr)
+ }
+ return outBuf.String(), errBuf.String()
+}
diff --git a/internals/daemon/api_exec_test.go b/internals/daemon/api_exec_test.go
index 23ede9ae..a7263837 100644
--- a/internals/daemon/api_exec_test.go
+++ b/internals/daemon/api_exec_test.go
@@ -23,7 +23,6 @@ import (
"os"
"os/user"
"path/filepath"
- "strconv"
"strings"
"time"
@@ -258,62 +257,6 @@ func (s *execSuite) TestCurrentUserGroup(c *C) {
c.Check(stderr, Equals, "")
}
-// See .github/workflows/tests.yml for how to run this test as root.
-func (s *execSuite) TestUserGroup(c *C) {
- if os.Getuid() != 0 {
- c.Skip("requires running as root")
- }
- username := os.Getenv("PEBBLE_TEST_USER")
- group := os.Getenv("PEBBLE_TEST_GROUP")
- if username == "" || group == "" {
- c.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP")
- }
- stdout, stderr, waitErr := s.exec(c, "", &client.ExecOptions{
- Command: []string{"/bin/sh", "-c", "id -n -u && id -n -g"},
- User: username,
- Group: group,
- })
- c.Assert(waitErr, IsNil)
- c.Check(stdout, Equals, username+"\n"+group+"\n")
- c.Check(stderr, Equals, "")
-
- _, err := s.client.Exec(&client.ExecOptions{
- Command: []string{"pwd"},
- Environment: map[string]string{"HOME": "/non/existent"},
- User: username,
- Group: group,
- })
- c.Assert(err, ErrorMatches, `.*home directory.*does not exist`)
-}
-
-// See .github/workflows/tests.yml for how to run this test as root.
-func (s *execSuite) TestUserIDGroupID(c *C) {
- if os.Getuid() != 0 {
- c.Skip("requires running as root")
- }
- username := os.Getenv("PEBBLE_TEST_USER")
- group := os.Getenv("PEBBLE_TEST_GROUP")
- if username == "" || group == "" {
- c.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP")
- }
- u, err := user.Lookup(username)
- c.Assert(err, IsNil)
- g, err := user.LookupGroup(group)
- c.Assert(err, IsNil)
- uid, err := strconv.Atoi(u.Uid)
- c.Assert(err, IsNil)
- gid, err := strconv.Atoi(g.Gid)
- c.Assert(err, IsNil)
- stdout, stderr, waitErr := s.exec(c, "", &client.ExecOptions{
- Command: []string{"/bin/sh", "-c", "id -n -u && id -n -g"},
- UserID: &uid,
- GroupID: &gid,
- })
- c.Assert(waitErr, IsNil)
- c.Check(stdout, Equals, username+"\n"+group+"\n")
- c.Check(stderr, Equals, "")
-}
-
func (s *execSuite) exec(c *C, stdin string, opts *client.ExecOptions) (stdout, stderr string, waitErr error) {
outBuf := &bytes.Buffer{}
errBuf := &bytes.Buffer{}
diff --git a/internals/daemon/api_files_root_test.go b/internals/daemon/api_files_root_test.go
new file mode 100644
index 00000000..c9bf878e
--- /dev/null
+++ b/internals/daemon/api_files_root_test.go
@@ -0,0 +1,480 @@
+//go:build roottest
+
+// Copyright (c) 2024 Canonical Ltd
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package daemon
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "os/user"
+ "strconv"
+ "syscall"
+ "testing"
+
+ "github.com/canonical/pebble/internals/osutil"
+)
+
+func TestWithRootMakeDirsUserGroupReal(t *testing.T) {
+ if os.Getuid() != 0 {
+ t.Skip("requires running as root")
+ }
+ username := os.Getenv("PEBBLE_TEST_USER")
+ group := os.Getenv("PEBBLE_TEST_GROUP")
+ if username == "" || group == "" {
+ t.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP")
+ }
+ u, err := user.Lookup(username)
+ if err != nil {
+ t.Fatalf("cannot look up username: %v", err)
+ }
+ g, err := user.LookupGroup(group)
+ if err != nil {
+ t.Fatalf("cannot look up group: %v", err)
+ }
+ uid, err := strconv.Atoi(u.Uid)
+ if err != nil {
+ t.Fatalf("cannot convert uid to int: %v", err)
+ }
+ gid, err := strconv.Atoi(g.Gid)
+ if err != nil {
+ t.Fatalf("cannot convert gid to int: %v", err)
+ }
+
+ tmpDir := testWithRootMakeDirsUserGroup(t, uid, gid, username, group)
+
+ info, err := os.Stat(tmpDir + "/normal")
+ if err != nil {
+ t.Fatalf("cannot stat dir %s: %v", tmpDir+"/normal", err)
+ }
+ statT := info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(0) {
+ t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/normal", uint32(0), statT.Uid)
+ }
+ if statT.Gid != uint32(0) {
+ t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/normal", uint32(0), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/uid-gid")
+ if err != nil {
+ t.Fatalf("cannot stat dir %s: %v", tmpDir+"/uid-gid", err)
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(uid) {
+ t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/uid-gid", uint32(uid), statT.Uid)
+ }
+ if statT.Gid != uint32(gid) {
+ t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/uid-gid", uint32(gid), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/user-group")
+ if err != nil {
+ t.Fatalf("cannot stat dir %s: %v", tmpDir+"/user-group", err)
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(uid) {
+ t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/user-group", uint32(uid), statT.Uid)
+ }
+ if statT.Gid != uint32(gid) {
+ t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/user-group", uint32(gid), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/nested1")
+ if err != nil {
+ t.Fatalf("cannot stat dir %s: %v", tmpDir+"/nested1", err)
+ }
+ if int(info.Mode()&os.ModePerm) != 0o755 {
+ t.Fatalf("dir %s mode error, expected: %v, got: %v", tmpDir+"/nested1", 0o755, int(info.Mode()&os.ModePerm))
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(0) {
+ t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/nested1", uint32(0), statT.Uid)
+ }
+ if statT.Gid != uint32(0) {
+ t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/nested1", uint32(0), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/nested1/normal")
+ if err != nil {
+ t.Fatalf("cannot stat dir %s: %v", tmpDir+"/nested1/normal", err)
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(0) {
+ t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/nested1/normal", uint32(0), statT.Uid)
+ }
+ if statT.Gid != uint32(0) {
+ t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/nested1/normal", uint32(0), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/nested2")
+ if err != nil {
+ t.Fatalf("cannot stat dir %s: %v", tmpDir+"/nested2", err)
+ }
+ if int(info.Mode()&os.ModePerm) != 0o755 {
+ t.Fatalf("dir %s mode error, expected: %v, got: %v", tmpDir+"/nested2", 0o755, int(info.Mode()&os.ModePerm))
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(uid) {
+ t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/nested2", uint32(uid), statT.Uid)
+ }
+ if statT.Gid != uint32(gid) {
+ t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/nested2", uint32(gid), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/nested2/user-group")
+ if err != nil {
+ t.Fatalf("cannot stat dir %s: %v", tmpDir+"/nested2/user-group", err)
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(uid) {
+ t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/nested2/user-group", uint32(uid), statT.Uid)
+ }
+ if statT.Gid != uint32(gid) {
+ t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/nested2/user-group", uint32(gid), statT.Uid)
+ }
+}
+
+func testWithRootMakeDirsUserGroup(t *testing.T, uid, gid int, user, group string) string {
+ tmpDir := t.TempDir()
+
+ headers := http.Header{
+ "Content-Type": []string{"application/json"},
+ }
+ payload := struct {
+ Action string
+ Dirs []makeDirsItem
+ }{
+ Action: "make-dirs",
+ Dirs: []makeDirsItem{
+ {Path: tmpDir + "/normal"},
+ {Path: tmpDir + "/uid-gid", UserID: &uid, GroupID: &gid},
+ {Path: tmpDir + "/user-group", User: user, Group: group},
+ {Path: tmpDir + "/nested1/normal", MakeParents: true},
+ {Path: tmpDir + "/nested2/user-group", User: user, Group: group, MakeParents: true},
+ },
+ }
+ reqBody, err := json.Marshal(payload)
+ if err != nil {
+ t.Fatalf("cannot marshal payload: %v", err)
+ }
+ body := doRequestRootTest(t, v1PostFiles, "POST", "/v1/files", nil, headers, reqBody)
+
+ var r testFilesResponse
+ if err := json.NewDecoder(body).Decode(&r); err != nil {
+ t.Fatalf("cannot decode response body for /v1/files: %v", err)
+ }
+ if r.StatusCode != http.StatusOK {
+ t.Fatalf("test file response status code error, expected: %v, got %v", http.StatusOK, r.StatusCode)
+
+ }
+ if r.Type != "sync" {
+ t.Fatalf("test file response type error, expected: sync, got %v", r.StatusCode)
+
+ }
+ if len(r.Result) != 5 {
+ t.Fatalf("test file response result length error, expected: 5, got %v", len(r.Result))
+
+ }
+ checkFileResultRootTest(t, r.Result[0], tmpDir+"/normal", "", "")
+ checkFileResultRootTest(t, r.Result[1], tmpDir+"/uid-gid", "", "")
+ checkFileResultRootTest(t, r.Result[2], tmpDir+"/user-group", "", "")
+ checkFileResultRootTest(t, r.Result[3], tmpDir+"/nested1/normal", "", "")
+ checkFileResultRootTest(t, r.Result[4], tmpDir+"/nested2/user-group", "", "")
+
+ if !osutil.IsDir(tmpDir + "/normal") {
+ t.Fatalf("file %s is not a directory", tmpDir+"/normal")
+
+ }
+ if !osutil.IsDir(tmpDir + "/uid-gid") {
+ t.Fatalf("file %s is not a directory", tmpDir+"/uid-gid")
+
+ }
+ if !osutil.IsDir(tmpDir + "/user-group") {
+ t.Fatalf("file %s is not a directory", tmpDir+"/user-group")
+
+ }
+ if !osutil.IsDir(tmpDir + "/nested1/normal") {
+ t.Fatalf("file %s is not a directory", tmpDir+"/nested1/normal")
+
+ }
+ if !osutil.IsDir(tmpDir + "/nested2/user-group") {
+ t.Fatalf("file %s is not a directory", tmpDir+"/nested2/user-group")
+
+ }
+
+ return tmpDir
+}
+
+func doRequestRootTest(t *testing.T, f ResponseFunc, method, url string, query url.Values, headers http.Header, body []byte) *bytes.Buffer {
+ t.Helper()
+
+ var bodyReader io.Reader
+ if body != nil {
+ bodyReader = bytes.NewBuffer(body)
+ }
+ req, err := http.NewRequest(method, url, bodyReader)
+ if err != nil {
+ t.Fatalf("http request error: %s", err)
+ }
+ if query != nil {
+ req.URL.RawQuery = query.Encode()
+ }
+ req.Header = headers
+ handler := f(apiCmd(url), req, nil)
+ recorder := httptest.NewRecorder()
+ handler.ServeHTTP(recorder, req)
+ response := recorder.Result()
+ if response.StatusCode != http.StatusOK {
+ t.Fatalf("http request to %s failed: %v", url, err)
+ }
+ return recorder.Body
+}
+
+func checkFileResultRootTest(t *testing.T, r testFileResult, path, errorKind, errorMsg string) {
+ t.Helper()
+
+ if r.Path != path {
+ t.Fatalf("error checking test file path, eexpected: %v, got: %v", path, r.Path)
+ }
+ if r.Error.Kind != errorKind {
+ t.Fatalf("error checking test file error kind, eexpected: %v, got: %v", errorKind, r.Error.Kind)
+ }
+ if r.Error.Message != errorMsg {
+ t.Fatalf("error checking test file error message, eexpected: %v, got: %v", errorMsg, r.Error.Message)
+ }
+}
+
+func TestWithRootWriteUserGroupReal(t *testing.T) {
+ if os.Getuid() != 0 {
+ t.Skip("requires running as root")
+ }
+ username := os.Getenv("PEBBLE_TEST_USER")
+ group := os.Getenv("PEBBLE_TEST_GROUP")
+ if username == "" || group == "" {
+ t.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP")
+ }
+ u, err := user.Lookup(username)
+ if err != nil {
+ t.Fatalf("cannot look up username: %v", err)
+ }
+ g, err := user.LookupGroup(group)
+ if err != nil {
+ t.Fatalf("cannot look up group: %v", err)
+ }
+ uid, err := strconv.Atoi(u.Uid)
+ if err != nil {
+ t.Fatalf("cannot convert uid to int: %v", err)
+ }
+ gid, err := strconv.Atoi(g.Gid)
+ if err != nil {
+ t.Fatalf("cannot convert gid to int: %v", err)
+ }
+
+ tmpDir := testWriteUserGroupRootTest(t, uid, gid, username, group)
+
+ info, err := os.Stat(tmpDir + "/normal")
+ if err != nil {
+ t.Fatalf("cannot stat file %s: %v", tmpDir+"/normal", err)
+ }
+ statT := info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(0) {
+ t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/normal", uint32(0), statT.Uid)
+ }
+ if statT.Gid != uint32(0) {
+ t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/normal", uint32(0), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/uid-gid")
+ if err != nil {
+ t.Fatalf("cannot stat file %s: %v", tmpDir+"/uid-gid", err)
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(uid) {
+ t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/uid-gid", uint32(uid), statT.Uid)
+ }
+ if statT.Gid != uint32(gid) {
+ t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/uid-gid", uint32(gid), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/user-group")
+ if err != nil {
+ t.Fatalf("cannot stat file %s: %v", tmpDir+"/user-group", err)
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(uid) {
+ t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/user-group", uint32(uid), statT.Uid)
+ }
+ if statT.Gid != uint32(gid) {
+ t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/user-group", uint32(gid), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/nested1")
+ if err != nil {
+ t.Fatalf("cannot stat file %s: %v", tmpDir+"/nested1", err)
+ }
+ if int(info.Mode()&os.ModePerm) != 0o755 {
+ t.Fatalf("file %s mode error, expected: %v, got: %v", tmpDir+"/nested1", 0o755, int(info.Mode()&os.ModePerm))
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(0) {
+ t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/nested1", uint32(0), statT.Uid)
+ }
+ if statT.Gid != uint32(0) {
+ t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/nested1", uint32(0), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/nested1/normal")
+ if err != nil {
+ t.Fatalf("cannot stat file %s: %v", tmpDir+"/nested1/normal", err)
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(0) {
+ t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/nested1/normal", uint32(0), statT.Uid)
+ }
+ if statT.Gid != uint32(0) {
+ t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/nested1/normal", uint32(0), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/nested2")
+ if err != nil {
+ t.Fatalf("cannot stat file %s: %v", tmpDir+"/nested2", err)
+ }
+ if int(info.Mode()&os.ModePerm) != 0o755 {
+ t.Fatalf("file %s mode error, expected: %v, got: %v", tmpDir+"/nested2", 0o755, int(info.Mode()&os.ModePerm))
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(uid) {
+ t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/nested2", uint32(uid), statT.Uid)
+ }
+ if statT.Gid != uint32(gid) {
+ t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/nested2", uint32(gid), statT.Uid)
+ }
+
+ info, err = os.Stat(tmpDir + "/nested2/user-group")
+ if err != nil {
+ t.Fatalf("cannot stat file %s: %v", tmpDir+"/nested2/user-group", err)
+ }
+ statT = info.Sys().(*syscall.Stat_t)
+ if statT.Uid != uint32(uid) {
+ t.Fatalf("file %s uid error, expected: %v, got: %v", tmpDir+"/nested2/user-group", uint32(uid), statT.Uid)
+ }
+ if statT.Gid != uint32(gid) {
+ t.Fatalf("file %s gid error, expected: %v, got: %v", tmpDir+"/nested2/user-group", uint32(gid), statT.Uid)
+ }
+}
+
+func testWriteUserGroupRootTest(t *testing.T, uid, gid int, user, group string) string {
+ tmpDir := t.TempDir()
+ pathNormal := tmpDir + "/normal"
+ pathUidGid := tmpDir + "/uid-gid"
+ pathUserGroup := tmpDir + "/user-group"
+ pathNested := tmpDir + "/nested1/normal"
+ pathNestedUserGroup := tmpDir + "/nested2/user-group"
+
+ headers := http.Header{
+ "Content-Type": []string{"multipart/form-data; boundary=01234567890123456789012345678901"},
+ }
+ body := doRequestRootTest(t, v1PostFiles, "POST", "/v1/files", nil, headers,
+ []byte(fmt.Sprintf(`
+--01234567890123456789012345678901
+Content-Disposition: form-data; name="request"
+
+{
+ "action": "write",
+ "files": [
+ {"path": "%[1]s"},
+ {"path": "%[2]s", "user-id": %[3]d, "group-id": %[4]d},
+ {"path": "%[5]s", "user": "%[6]s", "group": "%[7]s"},
+ {"path": "%[8]s", "make-dirs": true},
+ {"path": "%[9]s", "user": "%[10]s", "group": "%[11]s", "make-dirs": true}
+ ]
+}
+--01234567890123456789012345678901
+Content-Disposition: form-data; name="files"; filename="%[1]s"
+
+normal
+--01234567890123456789012345678901
+Content-Disposition: form-data; name="files"; filename="%[2]s"
+
+uid gid
+--01234567890123456789012345678901
+Content-Disposition: form-data; name="files"; filename="%[5]s"
+
+user group
+--01234567890123456789012345678901
+Content-Disposition: form-data; name="files"; filename="%[8]s"
+
+nested
+--01234567890123456789012345678901
+Content-Disposition: form-data; name="files"; filename="%[9]s"
+
+nested user group
+--01234567890123456789012345678901--
+`, pathNormal, pathUidGid, uid, gid, pathUserGroup, user, group,
+ pathNested, pathNestedUserGroup, user, group)))
+
+ var r testFilesResponse
+ if err := json.NewDecoder(body).Decode(&r); err != nil {
+ t.Fatalf("cannot decode response body for /v1/files: %v", err)
+ }
+ if r.StatusCode != http.StatusOK {
+ t.Fatalf("test file response status code error, expected: %v, got %v", http.StatusOK, r.StatusCode)
+
+ }
+ if r.Type != "sync" {
+ t.Fatalf("test file response type error, expected: sync, got %v", r.StatusCode)
+
+ }
+ if len(r.Result) != 5 {
+ t.Fatalf("test file response result length error, expected: 5, got %v", len(r.Result))
+
+ }
+ checkFileResultRootTest(t, r.Result[0], pathNormal, "", "")
+ checkFileResultRootTest(t, r.Result[1], pathUidGid, "", "")
+ checkFileResultRootTest(t, r.Result[2], pathUserGroup, "", "")
+ checkFileResultRootTest(t, r.Result[3], pathNested, "", "")
+ checkFileResultRootTest(t, r.Result[4], pathNestedUserGroup, "", "")
+
+ assertFileRootTest(t, pathNormal, 0o644, "normal")
+ assertFileRootTest(t, pathUidGid, 0o644, "uid gid")
+ assertFileRootTest(t, pathUserGroup, 0o644, "user group")
+ assertFileRootTest(t, pathNested, 0o644, "nested")
+ assertFileRootTest(t, pathNestedUserGroup, 0o644, "nested user group")
+
+ return tmpDir
+}
+
+func assertFileRootTest(t *testing.T, path string, perm os.FileMode, content string) {
+ b, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("cannot read file %s: %v", path, err)
+ }
+ if string(b) != content {
+ t.Fatalf("file content error, expected: %v, got: %v", content, string(b))
+ }
+ info, err := os.Stat(path)
+ if err != nil {
+ t.Fatalf("cannot stat file %s: %v", path, err)
+ }
+ if info.Mode().Perm() != perm {
+ t.Fatalf("error checking permission, expected: %v, got: %v", perm, info.Mode().Perm())
+ }
+}
diff --git a/internals/daemon/api_files_test.go b/internals/daemon/api_files_test.go
index 79c0f97e..2f4ba673 100644
--- a/internals/daemon/api_files_test.go
+++ b/internals/daemon/api_files_test.go
@@ -531,72 +531,6 @@ func (s *filesSuite) testMakeDirsUserGroup(c *C, uid, gid int, user, group strin
return tmpDir
}
-// See .github/workflows/tests.yml for how to run this test as root.
-func (s *filesSuite) TestMakeDirsUserGroupReal(c *C) {
- if os.Getuid() != 0 {
- c.Skip("requires running as root")
- }
- username := os.Getenv("PEBBLE_TEST_USER")
- group := os.Getenv("PEBBLE_TEST_GROUP")
- if username == "" || group == "" {
- c.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP")
- }
- u, err := user.Lookup(username)
- c.Assert(err, IsNil)
- g, err := user.LookupGroup(group)
- c.Assert(err, IsNil)
- uid, err := strconv.Atoi(u.Uid)
- c.Assert(err, IsNil)
- gid, err := strconv.Atoi(g.Gid)
- c.Assert(err, IsNil)
-
- tmpDir := s.testMakeDirsUserGroup(c, uid, gid, username, group)
-
- info, err := os.Stat(tmpDir + "/normal")
- c.Assert(err, IsNil)
- statT := info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(0))
- c.Check(statT.Gid, Equals, uint32(0))
-
- info, err = os.Stat(tmpDir + "/uid-gid")
- c.Assert(err, IsNil)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(uid))
- c.Check(statT.Gid, Equals, uint32(uid))
-
- info, err = os.Stat(tmpDir + "/user-group")
- c.Assert(err, IsNil)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(uid))
- c.Check(statT.Gid, Equals, uint32(uid))
-
- info, err = os.Stat(tmpDir + "/nested1")
- c.Assert(err, IsNil)
- c.Check(int(info.Mode()&os.ModePerm), Equals, 0o755)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(0))
- c.Check(statT.Gid, Equals, uint32(0))
-
- info, err = os.Stat(tmpDir + "/nested1/normal")
- c.Assert(err, IsNil)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(0))
- c.Check(statT.Gid, Equals, uint32(0))
-
- info, err = os.Stat(tmpDir + "/nested2")
- c.Assert(err, IsNil)
- c.Check(int(info.Mode()&os.ModePerm), Equals, 0o755)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(uid))
- c.Check(statT.Gid, Equals, uint32(gid))
-
- info, err = os.Stat(tmpDir + "/nested2/user-group")
- c.Assert(err, IsNil)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(uid))
- c.Check(statT.Gid, Equals, uint32(gid))
-}
-
func (s *filesSuite) TestRemoveSingle(c *C) {
tmpDir := c.MkDir()
writeTempFile(c, tmpDir, "file", "a", 0o644)
@@ -988,72 +922,6 @@ func (s *filesSuite) TestWriteUserGroupMocked(c *C) {
c.Check(mkdirCalls[1], Equals, mkdirArgs{tmpDir + "/nested2", 0o755, osutil.MkdirOptions{MakeParents: true, ExistOK: true, Chmod: true, Chown: true, UserID: 56, GroupID: 78}})
}
-// See .github/workflows/tests.yml for how to run this test as root.
-func (s *filesSuite) TestWriteUserGroupReal(c *C) {
- if os.Getuid() != 0 {
- c.Skip("requires running as root")
- }
- username := os.Getenv("PEBBLE_TEST_USER")
- group := os.Getenv("PEBBLE_TEST_GROUP")
- if username == "" || group == "" {
- c.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP")
- }
- u, err := user.Lookup(username)
- c.Assert(err, IsNil)
- g, err := user.LookupGroup(group)
- c.Assert(err, IsNil)
- uid, err := strconv.Atoi(u.Uid)
- c.Assert(err, IsNil)
- gid, err := strconv.Atoi(g.Gid)
- c.Assert(err, IsNil)
-
- tmpDir := s.testWriteUserGroup(c, uid, gid, username, group)
-
- info, err := os.Stat(tmpDir + "/normal")
- c.Assert(err, IsNil)
- statT := info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(0))
- c.Check(statT.Gid, Equals, uint32(0))
-
- info, err = os.Stat(tmpDir + "/uid-gid")
- c.Assert(err, IsNil)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(uid))
- c.Check(statT.Gid, Equals, uint32(uid))
-
- info, err = os.Stat(tmpDir + "/user-group")
- c.Assert(err, IsNil)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(uid))
- c.Check(statT.Gid, Equals, uint32(uid))
-
- info, err = os.Stat(tmpDir + "/nested1")
- c.Assert(err, IsNil)
- c.Check(int(info.Mode()&os.ModePerm), Equals, 0o755)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(0))
- c.Check(statT.Gid, Equals, uint32(0))
-
- info, err = os.Stat(tmpDir + "/nested1/normal")
- c.Assert(err, IsNil)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(0))
- c.Check(statT.Gid, Equals, uint32(0))
-
- info, err = os.Stat(tmpDir + "/nested2")
- c.Assert(err, IsNil)
- c.Check(int(info.Mode()&os.ModePerm), Equals, 0o755)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(uid))
- c.Check(statT.Gid, Equals, uint32(gid))
-
- info, err = os.Stat(tmpDir + "/nested2/user-group")
- c.Assert(err, IsNil)
- statT = info.Sys().(*syscall.Stat_t)
- c.Check(statT.Uid, Equals, uint32(uid))
- c.Check(statT.Gid, Equals, uint32(gid))
-}
-
func (s *filesSuite) testWriteUserGroup(c *C, uid, gid int, user, group string) string {
tmpDir := c.MkDir()
pathNormal := tmpDir + "/normal"
diff --git a/internals/osutil/mkdir_root_test.go b/internals/osutil/mkdir_root_test.go
new file mode 100644
index 00000000..d3148e5f
--- /dev/null
+++ b/internals/osutil/mkdir_root_test.go
@@ -0,0 +1,111 @@
+//go:build roottest
+
+// Copyright (c) 2024 Canonical Ltd
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 3 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package osutil_test
+
+import (
+ "os"
+ "os/user"
+ "strconv"
+ "syscall"
+ "testing"
+
+ "github.com/canonical/pebble/internals/osutil"
+ "github.com/canonical/pebble/internals/osutil/sys"
+)
+
+func TestWithRootMakeParentsChmodAndChown(t *testing.T) {
+ if os.Getuid() != 0 {
+ t.Skip("requires running as root")
+ }
+
+ username := os.Getenv("PEBBLE_TEST_USER")
+ group := os.Getenv("PEBBLE_TEST_GROUP")
+ if username == "" || group == "" {
+ t.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP")
+ }
+
+ u, err := user.Lookup(username)
+ if err != nil {
+ t.Fatalf("cannot look up username: %v", err)
+ }
+ g, err := user.LookupGroup(group)
+ if err != nil {
+ t.Fatalf("cannot look up group: %v", err)
+ }
+ uid, err := strconv.Atoi(u.Uid)
+ if err != nil {
+ t.Fatalf("cannot convert uid to int: %v", err)
+ }
+ gid, err := strconv.Atoi(g.Gid)
+ if err != nil {
+ t.Fatalf("cannot convert gid to int: %v", err)
+ }
+ tmpDir := t.TempDir()
+
+ err = osutil.Mkdir(tmpDir+"/foo/bar", 0o777, &osutil.MkdirOptions{
+ MakeParents: true,
+ Chmod: true,
+ Chown: true,
+ UserID: sys.UserID(uid),
+ GroupID: sys.GroupID(gid),
+ })
+ if err != nil {
+ t.Fatalf(": %v", err)
+ }
+ if !osutil.IsDir(tmpDir + "/foo") {
+ t.Fatalf("file %s is not a directory", tmpDir+"/foo")
+ }
+ if !osutil.IsDir(tmpDir + "/foo/bar") {
+ t.Fatalf("file %s is not a directory", tmpDir+"/foo/bar")
+ }
+
+ info, err := os.Stat(tmpDir + "/foo")
+ if err != nil {
+ t.Fatalf("cannot stat dir %s: %v", tmpDir+"/foo", err)
+ }
+ if info.Mode().Perm() != os.FileMode(0o777) {
+ t.Fatalf("error checking dir %s permission, expected: %v, got: %v", tmpDir+"/foo", 0o777, info.Mode().Perm())
+ }
+ stat, ok := info.Sys().(*syscall.Stat_t)
+ if !ok {
+ t.Fatalf("syscall stat on dir %s error", tmpDir+"/foo")
+ }
+ if int(stat.Uid) != uid {
+ t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/foo", uid, int(stat.Uid))
+ }
+ if int(stat.Uid) != uid {
+ t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/foo", gid, int(stat.Gid))
+ }
+
+ info, err = os.Stat(tmpDir + "/foo/bar")
+ if err != nil {
+ t.Fatalf(": %v", err)
+ }
+ if info.Mode().Perm() != os.FileMode(0o777) {
+ t.Fatalf("error checking dir %s permission, expected: %v, got: %v", tmpDir+"/foo/bar", 0o777, info.Mode().Perm())
+ }
+ stat, ok = info.Sys().(*syscall.Stat_t)
+ if !ok {
+ t.Fatalf("syscall stat on dir %s error", tmpDir+"/foo/bar")
+ }
+ if int(stat.Uid) != uid {
+ t.Fatalf("dir %s uid error, expected: %v, got: %v", tmpDir+"/foo/bar", uid, int(stat.Uid))
+ }
+ if int(stat.Uid) != uid {
+ t.Fatalf("dir %s gid error, expected: %v, got: %v", tmpDir+"/foo/bar", gid, int(stat.Gid))
+ }
+}
diff --git a/internals/osutil/mkdir_test.go b/internals/osutil/mkdir_test.go
index 6983e26d..668a31ba 100644
--- a/internals/osutil/mkdir_test.go
+++ b/internals/osutil/mkdir_test.go
@@ -16,14 +16,11 @@ package osutil_test
import (
"os"
- "os/user"
- "strconv"
"syscall"
"gopkg.in/check.v1"
"github.com/canonical/pebble/internals/osutil"
- "github.com/canonical/pebble/internals/osutil/sys"
)
type mkdirSuite struct{}
@@ -207,53 +204,3 @@ func (mkdirSuite) TestMakeParentsAndNoChmod(c *check.C) {
c.Assert(err, check.IsNil)
c.Assert(info.Mode().Perm(), check.Equals, os.FileMode(0o755))
}
-
-// See .github/workflows/tests.yml for how to run this test as root.
-func (mkdirSuite) TestMakeParentsChmodAndChown(c *check.C) {
- if os.Getuid() != 0 {
- c.Skip("requires running as root")
- }
-
- username := os.Getenv("PEBBLE_TEST_USER")
- group := os.Getenv("PEBBLE_TEST_GROUP")
- if username == "" || group == "" {
- c.Fatalf("must set PEBBLE_TEST_USER and PEBBLE_TEST_GROUP")
- }
-
- u, err := user.Lookup(username)
- c.Assert(err, check.IsNil)
- g, err := user.LookupGroup(group)
- c.Assert(err, check.IsNil)
- uid, err := strconv.Atoi(u.Uid)
- c.Assert(err, check.IsNil)
- gid, err := strconv.Atoi(g.Gid)
- c.Assert(err, check.IsNil)
- tmpDir := c.MkDir()
-
- err = osutil.Mkdir(tmpDir+"/foo/bar", 0o777, &osutil.MkdirOptions{
- MakeParents: true,
- Chmod: true,
- Chown: true,
- UserID: sys.UserID(uid),
- GroupID: sys.GroupID(gid),
- })
- c.Assert(err, check.IsNil)
- c.Assert(osutil.IsDir(tmpDir+"/foo"), check.Equals, true)
- c.Assert(osutil.IsDir(tmpDir+"/foo/bar"), check.Equals, true)
-
- info, err := os.Stat(tmpDir + "/foo")
- c.Assert(err, check.IsNil)
- c.Assert(info.Mode().Perm(), check.Equals, os.FileMode(0o777))
- stat, ok := info.Sys().(*syscall.Stat_t)
- c.Assert(ok, check.Equals, true)
- c.Assert(int(stat.Uid), check.Equals, uid)
- c.Assert(int(stat.Gid), check.Equals, gid)
-
- info, err = os.Stat(tmpDir + "/foo/bar")
- c.Assert(err, check.IsNil)
- c.Assert(info.Mode().Perm(), check.Equals, os.FileMode(0o777))
- stat, ok = info.Sys().(*syscall.Stat_t)
- c.Assert(ok, check.Equals, true)
- c.Assert(int(stat.Uid), check.Equals, uid)
- c.Assert(int(stat.Gid), check.Equals, gid)
-}