Skip to content

Commit 2736b74

Browse files
rashedkvmhiddeco
andcommitted
storage: set 0o744 for files with exec mode set
This commit ensures that files with exec permissions set continue to be executable by the user extracting the archive. This is not of use to any of Flux itself, but does help downstream dependents making use of the controller to facilitate artifact acquisitions for their (CI/CD) software suite. Co-authored-by: Hidde Beydals <[email protected]> Signed-off-by: Rashed Kamal <[email protected]>
1 parent 8d9b0f4 commit 2736b74

File tree

2 files changed

+113
-50
lines changed

2 files changed

+113
-50
lines changed

internal/controller/storage.go

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ import (
4848
const GarbageCountLimit = 1000
4949

5050
const (
51-
// defaultFileMode is the permission mode applied to all files inside an artifact archive.
51+
// defaultFileMode is the permission mode applied to files inside an artifact archive.
5252
defaultFileMode int64 = 0o644
5353
// defaultDirMode is the permission mode applied to all directories inside an artifact archive.
5454
defaultDirMode int64 = 0o755
55+
// defaultExeFileMode is the permission mode applied to executable files inside an artifact archive.
56+
defaultExeFileMode int64 = 0o744
5557
)
5658

5759
// Storage manages artifacts
@@ -424,6 +426,7 @@ func (s Storage) Archive(artifact *v1.Artifact, dir string, filter ArchiveFileFi
424426
if err != nil {
425427
return err
426428
}
429+
427430
// The name needs to be modified to maintain directory structure
428431
// as tar.FileInfoHeader only has access to the base name of the file.
429432
// Ref: https://golang.org/src/archive/tar/common.go?#L626
@@ -434,21 +437,7 @@ func (s Storage) Archive(artifact *v1.Artifact, dir string, filter ArchiveFileFi
434437
return err
435438
}
436439
}
437-
header.Name = relFilePath
438-
439-
// We want to remove any environment specific data as well, this
440-
// ensures the checksum is purely content based.
441-
header.Gid = 0
442-
header.Uid = 0
443-
header.Uname = ""
444-
header.Gname = ""
445-
header.ModTime = time.Time{}
446-
header.AccessTime = time.Time{}
447-
header.ChangeTime = time.Time{}
448-
header.Mode = defaultFileMode
449-
if fi.Mode().IsDir() {
450-
header.Mode = defaultDirMode
451-
}
440+
sanitizeHeader(relFilePath, header)
452441

453442
if err := tw.WriteHeader(header); err != nil {
454443
return err
@@ -689,3 +678,42 @@ func (wc *writeCounter) Write(p []byte) (int, error) {
689678
wc.written += int64(n)
690679
return n, nil
691680
}
681+
682+
// sanitizeHeader modifies the tar.Header to be relative to the root of the
683+
// archive and removes any environment specific data.
684+
func sanitizeHeader(relP string, h *tar.Header) {
685+
// Modify the name to be relative to the root of the archive,
686+
// this ensures we maintain the same structure when extracting.
687+
h.Name = relP
688+
689+
// We want to remove any environment specific data as well, this
690+
// ensures the checksum is purely content based.
691+
h.Gid = 0
692+
h.Uid = 0
693+
h.Uname = ""
694+
h.Gname = ""
695+
h.ModTime = time.Time{}
696+
h.AccessTime = time.Time{}
697+
h.ChangeTime = time.Time{}
698+
699+
// Override the mode to be the default for the type of file.
700+
setDefaultMode(h)
701+
}
702+
703+
// setDefaultMode sets the default mode for the given header.
704+
func setDefaultMode(h *tar.Header) {
705+
if h.FileInfo().IsDir() {
706+
h.Mode = defaultDirMode
707+
return
708+
}
709+
710+
if h.FileInfo().Mode().IsRegular() {
711+
mode := h.FileInfo().Mode()
712+
if mode&os.ModeType == 0 && mode&0o111 != 0 {
713+
h.Mode = defaultExeFileMode
714+
return
715+
}
716+
h.Mode = defaultFileMode
717+
return
718+
}
719+
}

internal/controller/storage_test.go

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,14 @@ func TestStorage_Archive(t *testing.T) {
109109
t.Fatalf("error while bootstrapping storage: %v", err)
110110
}
111111

112-
createFiles := func(files map[string][]byte) (dir string, err error) {
112+
type dummyFile struct {
113+
content []byte
114+
mode int64
115+
}
116+
117+
createFiles := func(files map[string]dummyFile) (dir string, err error) {
113118
dir = t.TempDir()
114-
for name, b := range files {
119+
for name, df := range files {
115120
absPath := filepath.Join(dir, name)
116121
if err = os.MkdirAll(filepath.Dir(absPath), 0o750); err != nil {
117122
return
@@ -120,18 +125,24 @@ func TestStorage_Archive(t *testing.T) {
120125
if err != nil {
121126
return "", fmt.Errorf("could not create file %q: %w", absPath, err)
122127
}
123-
if n, err := f.Write(b); err != nil {
128+
if n, err := f.Write(df.content); err != nil {
124129
f.Close()
125130
return "", fmt.Errorf("could not write %d bytes to file %q: %w", n, f.Name(), err)
126131
}
127132
f.Close()
133+
134+
if df.mode != 0 {
135+
if err = os.Chmod(absPath, os.FileMode(df.mode)); err != nil {
136+
return "", fmt.Errorf("could not chmod file %q: %w", absPath, err)
137+
}
138+
}
128139
}
129140
return
130141
}
131142

132-
matchFiles := func(t *testing.T, storage *Storage, artifact sourcev1.Artifact, files map[string][]byte, dirs []string) {
143+
matchFiles := func(t *testing.T, storage *Storage, artifact sourcev1.Artifact, files map[string]dummyFile, dirs []string) {
133144
t.Helper()
134-
for name, b := range files {
145+
for name, df := range files {
135146
mustExist := !(name[0:1] == "!")
136147
if !mustExist {
137148
name = name[1:]
@@ -140,7 +151,7 @@ func TestStorage_Archive(t *testing.T) {
140151
if err != nil {
141152
t.Fatalf("failed reading tarball: %v", err)
142153
}
143-
if bs := int64(len(b)); s != bs {
154+
if bs := int64(len(df.content)); s != bs {
144155
t.Fatalf("%q size %v != %v", name, s, bs)
145156
}
146157
if exist != mustExist {
@@ -150,8 +161,12 @@ func TestStorage_Archive(t *testing.T) {
150161
t.Errorf("tarball contained excluded file %q", name)
151162
}
152163
}
153-
if exist && m != defaultFileMode {
154-
t.Fatalf("%q mode %v != %v", name, m, defaultFileMode)
164+
expectMode := df.mode
165+
if expectMode == 0 {
166+
expectMode = defaultFileMode
167+
}
168+
if exist && m != expectMode {
169+
t.Fatalf("%q mode %v != %v", name, m, expectMode)
155170
}
156171
}
157172
for _, name := range dirs {
@@ -179,62 +194,62 @@ func TestStorage_Archive(t *testing.T) {
179194

180195
tests := []struct {
181196
name string
182-
files map[string][]byte
197+
files map[string]dummyFile
183198
filter ArchiveFileFilter
184-
want map[string][]byte
199+
want map[string]dummyFile
185200
wantDirs []string
186201
wantErr bool
187202
}{
188203
{
189204
name: "no filter",
190-
files: map[string][]byte{
191-
".git/config": nil,
192-
"file.jpg": []byte(`contents`),
193-
"manifest.yaml": nil,
205+
files: map[string]dummyFile{
206+
".git/config": {},
207+
"file.jpg": {content: []byte(`contents`)},
208+
"manifest.yaml": {},
194209
},
195210
filter: nil,
196-
want: map[string][]byte{
197-
".git/config": nil,
198-
"file.jpg": []byte(`contents`),
199-
"manifest.yaml": nil,
211+
want: map[string]dummyFile{
212+
".git/config": {},
213+
"file.jpg": {content: []byte(`contents`)},
214+
"manifest.yaml": {},
200215
},
201216
},
202217
{
203218
name: "exclude VCS",
204-
files: map[string][]byte{
205-
".git/config": nil,
206-
"manifest.yaml": nil,
219+
files: map[string]dummyFile{
220+
".git/config": {},
221+
"manifest.yaml": {},
207222
},
208223
wantDirs: []string{
209224
"!.git",
210225
},
211226
filter: SourceIgnoreFilter(nil, nil),
212-
want: map[string][]byte{
213-
"!.git/config": nil,
214-
"manifest.yaml": nil,
227+
want: map[string]dummyFile{
228+
"!.git/config": {},
229+
"manifest.yaml": {},
215230
},
216231
},
217232
{
218233
name: "custom",
219-
files: map[string][]byte{
220-
".git/config": nil,
221-
"custom": nil,
222-
"horse.jpg": nil,
234+
files: map[string]dummyFile{
235+
".git/config": {},
236+
"custom": {},
237+
"horse.jpg": {},
223238
},
224239
filter: SourceIgnoreFilter([]gitignore.Pattern{
225240
gitignore.ParsePattern("custom", nil),
226241
}, nil),
227-
want: map[string][]byte{
228-
"!git/config": nil,
229-
"!custom": nil,
230-
"horse.jpg": nil,
242+
want: map[string]dummyFile{
243+
"!git/config": {},
244+
"!custom": {},
245+
"horse.jpg": {},
231246
},
232247
wantErr: false,
233248
},
234249
{
235250
name: "including directories",
236-
files: map[string][]byte{
237-
"test/.gitkeep": nil,
251+
files: map[string]dummyFile{
252+
"test/.gitkeep": {},
238253
},
239254
filter: SourceIgnoreFilter([]gitignore.Pattern{
240255
gitignore.ParsePattern("custom", nil),
@@ -244,6 +259,26 @@ func TestStorage_Archive(t *testing.T) {
244259
},
245260
wantErr: false,
246261
},
262+
{
263+
name: "sets default file modes",
264+
files: map[string]dummyFile{
265+
"test/file": {
266+
mode: 0o666,
267+
},
268+
"test/executable": {
269+
mode: 0o777,
270+
},
271+
},
272+
want: map[string]dummyFile{
273+
"test/file": {
274+
mode: defaultFileMode,
275+
},
276+
"test/executable": {
277+
mode: defaultExeFileMode,
278+
},
279+
},
280+
wantErr: false,
281+
},
247282
}
248283
for _, tt := range tests {
249284
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)