Skip to content

Commit caa1896

Browse files
mafredrijohnstcn
andauthored
fix: ignore file ownership for copy from context (#29)
Fix for caching when context files permissions change. This is irrelevant for a COPY operation since the they are either copied as root:root, or a specific owner/group depending on COPY --chown= argument. Relates to coder/terraform-provider-envbuilder#43 Co-authored-by: Cian Johnston <[email protected]>
1 parent f307586 commit caa1896

14 files changed

+105
-37
lines changed

.github/workflows/unit-tests.yaml

+9-7
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: Unit tests
22

33
on:
44
push:
5-
branches: ['main']
5+
branches: ["main"]
66
pull_request:
7-
branches: ['main']
7+
branches: ["main"]
88

99
permissions:
1010
contents: read
@@ -13,8 +13,10 @@ jobs:
1313
tests:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v4.01
17-
with:
18-
go-version: '1.22'
19-
- uses: actions/checkout@b0e28b5ac45a892f91e7d036f8200cf5ed489415 # v3
20-
- run: make test
16+
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v4.01
17+
with:
18+
go-version: "1.22"
19+
- uses: actions/checkout@b0e28b5ac45a892f91e7d036f8200cf5ed489415 # v3
20+
# Some unit tests need to be run as root to function correctly as they
21+
# may attempt to run `chown`.
22+
- run: sudo make test

pkg/commands/add.go

+9
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ func (a *AddCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui
5656
chmod = fs.FileMode(0o600)
5757
}
5858

59+
// All files and directories copied from the build context are created with a
60+
// UID and GID of 0 unless the optional --chown flag specifies a given
61+
// username, groupname, or UID/GID combination to request specific ownership
62+
// of the copied content.
63+
// See also: ./copy.go#L57
64+
chownStr := a.cmd.Chown
65+
if chownStr == "" {
66+
chownStr = "0:0"
67+
}
5968
uid, gid, err := util.GetUserGroup(a.cmd.Chown, replacementEnvs)
6069
if err != nil {
6170
return errors.Wrap(err, "getting user group from chown")

pkg/commands/add_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ func setupAddTest(t *testing.T) string {
123123
}
124124

125125
func Test_AddCommand(t *testing.T) {
126+
if os.Getuid() != 0 {
127+
t.Skip("Test requires root privileges as it attempts to chown")
128+
}
126129
tempDir := setupAddTest(t)
127130

128131
fileContext := util.FileContext{Root: tempDir}

pkg/commands/copy.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,16 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu
5353
}
5454

5555
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
56-
uid, gid, err := getUserGroup(c.cmd.Chown, replacementEnvs)
56+
57+
// All files and directories copied from the build context are created with a
58+
// UID and GID of 0 unless the optional --chown flag specifies a given
59+
// username, groupname, or UID/GID combination to request specific ownership
60+
// of the copied content.
61+
chownStr := c.cmd.Chown
62+
if chownStr == "" && c.From() == "" {
63+
chownStr = "0:0"
64+
}
65+
uid, gid, err := getUserGroup(chownStr, replacementEnvs)
5766
logrus.Debugf("found uid %v and gid %v for chown string %v", uid, gid, c.cmd.Chown)
5867
if err != nil {
5968
return errors.Wrap(err, "getting user group from chown")

pkg/commands/copy_test.go

+9
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@ func Test_CachingCopyCommand_ExecuteCommand(t *testing.T) {
296296
}
297297

298298
func TestCopyExecuteCmd(t *testing.T) {
299+
if os.Getuid() != 0 {
300+
t.Skip("Test requires root privileges as it attempts to chown")
301+
}
299302
tempDir := setupTestTemp(t)
300303

301304
cfg := &v1.Config{
@@ -431,6 +434,9 @@ func Test_resolveIfSymlink(t *testing.T) {
431434
}
432435

433436
func Test_CopyEnvAndWildcards(t *testing.T) {
437+
if os.Getuid() != 0 {
438+
t.Skip("Test requires root privileges as it attempts to chown")
439+
}
434440
setupDirs := func(t *testing.T) (string, string) {
435441
testDir := t.TempDir()
436442

@@ -495,6 +501,9 @@ func Test_CopyEnvAndWildcards(t *testing.T) {
495501
}
496502

497503
func TestCopyCommand_ExecuteCommand_Extended(t *testing.T) {
504+
if os.Getuid() != 0 {
505+
t.Skip("Test requires root privileges as it attempts to chown")
506+
}
498507
setupDirs := func(t *testing.T) (string, string) {
499508
testDir := t.TempDir()
500509

pkg/executor/build.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,20 @@ func (s *stageBuilder) populateCompositeKey(command commands.DockerCommand, file
227227
}
228228
}
229229

230+
// We want to avoid mutating the entire file context for the rest of its
231+
// lifetime, just for adding to the cache key. Make a copy.
232+
addPathCtx := s.fileContext
233+
if f, ok := command.(interface{ From() string }); ok {
234+
if f.From() == "" {
235+
addPathCtx.IgnoreOwnerAndGroup = true
236+
}
237+
}
238+
230239
// Add the next command to the cache key.
231240
compositeKey.AddKey(command.String())
232241

233242
for _, f := range files {
234-
if err := compositeKey.AddPath(f, s.fileContext); err != nil {
243+
if err := compositeKey.AddPath(f, addPathCtx); err != nil {
235244
return compositeKey, err
236245
}
237246
}

pkg/executor/build_test.go

+30-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"archive/tar"
2121
"bytes"
2222
"fmt"
23+
"os"
2324
"path/filepath"
2425
"reflect"
2526
"sort"
@@ -948,6 +949,7 @@ func Test_stageBuilder_build(t *testing.T) {
948949
crossStageDeps map[int][]string
949950
mockGetFSFromImage func(root string, img v1.Image, extract util.ExtractFunction) ([]string, error)
950951
shouldInitSnapshot bool
952+
skipReason string
951953
}
952954

953955
testCases := []testcase{
@@ -1136,6 +1138,13 @@ func Test_stageBuilder_build(t *testing.T) {
11361138
}
11371139
}(),
11381140
func() testcase {
1141+
name := "copy command cache enabled and key is not in cache"
1142+
if os.Getuid() != 0 {
1143+
return testcase{
1144+
description: name,
1145+
skipReason: "test requires root, attempts chown",
1146+
}
1147+
}
11391148
dir, filenames := tempDirAndFile(t)
11401149
filename := filenames[0]
11411150
tarContent := []byte{}
@@ -1174,7 +1183,7 @@ COPY %s foo.txt
11741183

11751184
cmds := stage.Commands
11761185
return testcase{
1177-
description: "copy command cache enabled and key is not in cache",
1186+
description: name,
11781187
opts: opts,
11791188
config: &v1.ConfigFile{Config: v1.Config{WorkingDir: destDir}},
11801189
layerCache: &fakeLayerCache{},
@@ -1193,6 +1202,13 @@ COPY %s foo.txt
11931202
}
11941203
}(),
11951204
func() testcase {
1205+
name := "cached run command followed by uncached copy command results in consistent read and write hashes"
1206+
if os.Getuid() != 0 {
1207+
return testcase{
1208+
description: name,
1209+
skipReason: "test requires root, attempts chown",
1210+
}
1211+
}
11961212
dir, filenames := tempDirAndFile(t)
11971213
filename := filenames[0]
11981214
tarContent := generateTar(t, filename)
@@ -1251,7 +1267,7 @@ COPY %s bar.txt
12511267

12521268
cmds := stage.Commands
12531269
return testcase{
1254-
description: "cached run command followed by uncached copy command results in consistent read and write hashes",
1270+
description: name,
12551271
opts: &config.KanikoOptions{Cache: true, CacheCopyLayers: true, CacheRunLayers: true},
12561272
rootDir: dir,
12571273
config: &v1.ConfigFile{Config: v1.Config{WorkingDir: destDir}},
@@ -1267,6 +1283,14 @@ COPY %s bar.txt
12671283
}
12681284
}(),
12691285
func() testcase {
1286+
name := "copy command followed by cached run command results in consistent read and write hashes"
1287+
if os.Getuid() != 0 {
1288+
return testcase{
1289+
description: name,
1290+
skipReason: "test requires root, attempts chown",
1291+
}
1292+
}
1293+
12701294
dir, filenames := tempDirAndFile(t)
12711295
filename := filenames[0]
12721296
tarContent := generateTar(t, filename)
@@ -1325,7 +1349,7 @@ RUN foobar
13251349

13261350
cmds := stage.Commands
13271351
return testcase{
1328-
description: "copy command followed by cached run command results in consistent read and write hashes",
1352+
description: name,
13291353
opts: &config.KanikoOptions{Cache: true, CacheRunLayers: true},
13301354
rootDir: dir,
13311355
config: &v1.ConfigFile{Config: v1.Config{WorkingDir: destDir}},
@@ -1471,6 +1495,9 @@ RUN foobar
14711495
}
14721496
for _, tc := range testCases {
14731497
t.Run(tc.description, func(t *testing.T) {
1498+
if tc.skipReason != "" {
1499+
t.Skip(tc.skipReason)
1500+
}
14741501
var fileName string
14751502
if tc.commands == nil {
14761503
file, err := filesystem.CreateTemp("", "foo")

pkg/executor/cache_probe_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"net/http/httptest"
2222
"net/url"
23+
"os"
2324
"path/filepath"
2425
"strings"
2526
"testing"
@@ -34,6 +35,9 @@ import (
3435
)
3536

3637
func TestDoCacheProbe(t *testing.T) {
38+
if os.Getuid() != 0 {
39+
t.Skip("Test setup requires root privileges as it attempts to chown")
40+
}
3741
t.Run("Empty", func(t *testing.T) {
3842
testDir, fn := setupCacheProbeTests(t)
3943
defer fn()

pkg/executor/composite_cache.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (s *CompositeCache) AddPath(p string, context util.FileContext) error {
7979
if context.ExcludesFile(p) {
8080
return nil
8181
}
82-
fh, err := util.CacheHasher()(p)
82+
fh, err := util.CacheHasher(context.IgnoreOwnerAndGroup)(p)
8383
if err != nil {
8484
return err
8585
}
@@ -104,7 +104,7 @@ func hashDir(p string, context util.FileContext) (bool, string, error) {
104104
return nil
105105
}
106106

107-
fileHash, err := util.CacheHasher()(path)
107+
fileHash, err := util.CacheHasher(context.IgnoreOwnerAndGroup)(path)
108108
if err != nil {
109109
return err
110110
}

pkg/executor/copy_multistage_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ func readDirectory(dirName string) ([]fs.FileInfo, error) {
4848
}
4949

5050
func TestCopyCommand_Multistage(t *testing.T) {
51+
if os.Getuid() != 0 {
52+
t.Skip("Test requires root privileges as it attempts to chown")
53+
}
5154
t.Run("copy a file across multistage", func(t *testing.T) {
5255
testDir, fn := setupMultistageTests(t)
5356
defer fn()

pkg/util/command_util_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,7 @@ func TestGetUserGroup(t *testing.T) {
536536
description string
537537
chown string
538538
env []string
539+
from string
539540
mockIDGetter func(userStr string, groupStr string) (uint32, uint32, error)
540541
// needed, in case uid is a valid number, but group is a name
541542
mockGroupIDGetter func(groupStr string) (*user.Group, error)
@@ -571,8 +572,8 @@ func TestGetUserGroup(t *testing.T) {
571572
mockIDGetter: func(string, string) (uint32, uint32, error) {
572573
return 0, 0, fmt.Errorf("should not be called")
573574
},
574-
expectedU: -1,
575-
expectedG: -1,
575+
expectedU: DoNotChangeUID,
576+
expectedG: DoNotChangeGID,
576577
},
577578
}
578579
for _, tc := range tests {

pkg/util/fs_util.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,9 @@ var skipKanikoDir = func() otiai10Cpy.Options {
100100
}
101101

102102
type FileContext struct {
103-
Root string
104-
ExcludedFiles []string
103+
Root string
104+
ExcludedFiles []string
105+
IgnoreOwnerAndGroup bool
105106
}
106107

107108
type ExtractFunction func(string, *tar.Header, string, io.Reader) error

pkg/util/util.go

+2-15
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ import (
2424
"io"
2525
"math"
2626
"os"
27-
"path/filepath"
2827
"strconv"
29-
"strings"
3028
"sync"
3129
"syscall"
3230
"time"
@@ -96,7 +94,7 @@ type CacheHasherFileInfoSum interface {
9694
}
9795

9896
// CacheHasher takes into account everything the regular hasher does except for mtime
99-
func CacheHasher() func(string) (string, error) {
97+
func CacheHasher(ignoreOwnerAndGroup bool) func(string) (string, error) {
10098
hasher := func(p string) (string, error) {
10199
h := md5.New()
102100
fi, err := filesystem.FS.Lstat(p)
@@ -114,18 +112,7 @@ func CacheHasher() func(string) (string, error) {
114112

115113
h.Write([]byte(fi.Mode().String()))
116114

117-
// Cian: this is a disgusting hack, but it removes the need for the
118-
// envbuilder binary to be owned by root when doing a cache probe.
119-
// We want to ignore UID and GID changes for the envbuilder binary
120-
// specifically. When building and pushing an image using the envbuilder
121-
// image, the embedded envbuilder binary will most likely be owned by
122-
// root:root. However, when performing a cache probe operation, it is more
123-
// likely that the file will be owned by the UID/GID that is running
124-
// envbuilder, which in this case is not guaranteed to be root.
125-
// Let's just pretend that it is, cross our fingers, and hope for the best.
126-
lyingAboutOwnership := !fi.IsDir() &&
127-
strings.HasSuffix(filepath.Clean(filepath.Dir(p)), ".envbuilder.tmp")
128-
if lyingAboutOwnership {
115+
if ignoreOwnerAndGroup {
129116
h.Write([]byte(strconv.FormatUint(uint64(0), 36)))
130117
h.Write([]byte(","))
131118
h.Write([]byte(strconv.FormatUint(uint64(0), 36)))

scripts/test.sh

+8-4
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,21 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
17+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1818

1919
#set -e
2020

2121
RED='\033[0;31m'
2222
GREEN='\033[0;32m'
2323
RESET='\033[0m'
2424

25+
# Warn if the script is not running as root.
26+
if [[ $(id -u) != "0" ]]; then
27+
trap 'echo "WARN: Not run as root. Some tests were skipped!" 1>&2' EXIT
28+
fi
29+
2530
echo "Running go tests..."
26-
go test -cover -coverprofile=out/coverage.out -v -timeout 120s `go list ./... | grep -v integration` | sed ''/PASS/s//$(printf "${GREEN}PASS${RESET}")/'' | sed ''/FAIL/s//$(printf "${RED}FAIL${RESET}")/''
31+
go test -cover -coverprofile=out/coverage.out -v -timeout 120s $(go list ./... | grep -v integration) | sed ''/PASS/s//$(printf "${GREEN}PASS${RESET}")/'' | sed ''/FAIL/s//$(printf "${RED}FAIL${RESET}")/''
2732
GO_TEST_EXIT_CODE=${PIPESTATUS[0]}
2833
if [[ $GO_TEST_EXIT_CODE -ne 0 ]]; then
2934
exit $GO_TEST_EXIT_CODE
@@ -35,8 +40,7 @@ scripts=(
3540
"$DIR/../hack/gofmt.sh"
3641
)
3742
fail=0
38-
for s in "${scripts[@]}"
39-
do
43+
for s in "${scripts[@]}"; do
4044
echo "RUN ${s}"
4145
if "${s}"; then
4246
echo -e "${GREEN}PASSED${RESET} ${s}"

0 commit comments

Comments
 (0)