Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/content-service/pkg/archive/tar.go
2499 views
1
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package archive
6
7
import (
8
"archive/tar"
9
"context"
10
"io"
11
"os"
12
"os/exec"
13
"path"
14
"sort"
15
"strings"
16
"syscall"
17
"time"
18
19
"github.com/opentracing/opentracing-go"
20
"golang.org/x/sys/unix"
21
"golang.org/x/xerrors"
22
23
"github.com/gitpod-io/gitpod/common-go/log"
24
"github.com/gitpod-io/gitpod/common-go/tracing"
25
)
26
27
// TarConfig configures tarbal creation/extraction
28
type TarConfig struct {
29
UIDMaps []IDMapping
30
GIDMaps []IDMapping
31
}
32
33
// BuildTarbalOption configures the tarbal creation
34
type TarOption func(o *TarConfig)
35
36
// IDMapping maps user or group IDs
37
type IDMapping struct {
38
ContainerID int
39
HostID int
40
Size int
41
}
42
43
// WithUIDMapping reverses the given user ID mapping during archive creation
44
func WithUIDMapping(mappings []IDMapping) TarOption {
45
return func(o *TarConfig) {
46
o.UIDMaps = mappings
47
}
48
}
49
50
// WithGIDMapping reverses the given user ID mapping during archive creation
51
func WithGIDMapping(mappings []IDMapping) TarOption {
52
return func(o *TarConfig) {
53
o.GIDMaps = mappings
54
}
55
}
56
57
// ExtractTarbal extracts an OCI compatible tar file src to the folder dst, expecting the overlay whiteout format
58
func ExtractTarbal(ctx context.Context, src io.Reader, dst string, opts ...TarOption) (err error) {
59
type Info struct {
60
UID, GID int
61
IsSymlink bool
62
Xattrs map[string]string
63
}
64
65
//nolint:staticcheck,ineffassign
66
span, ctx := opentracing.StartSpanFromContext(ctx, "extractTarbal")
67
span.LogKV("dst", dst)
68
defer tracing.FinishSpan(span, &err)
69
70
var cfg TarConfig
71
start := time.Now()
72
for _, opt := range opts {
73
opt(&cfg)
74
}
75
76
pipeReader, pipeWriter := io.Pipe()
77
teeReader := io.TeeReader(src, pipeWriter)
78
79
tarReader := tar.NewReader(pipeReader)
80
81
finished := make(chan bool)
82
m := make(map[string]Info)
83
84
unpackSpan := opentracing.StartSpan("unpackTarbal", opentracing.ChildOf(span.Context()))
85
go func() {
86
defer close(finished)
87
for {
88
hdr, err := tarReader.Next()
89
if err == io.EOF {
90
finished <- true
91
return
92
}
93
94
if err != nil {
95
log.WithError(err).Error("error reading tar")
96
return
97
}
98
99
m[hdr.Name] = Info{
100
UID: hdr.Uid,
101
GID: hdr.Gid,
102
IsSymlink: (hdr.Linkname != ""),
103
//nolint:staticcheck
104
Xattrs: hdr.Xattrs,
105
}
106
}
107
}()
108
109
// Be explicit about the tar flags. We want to restore the exact content without changes
110
tarcmd := exec.Command(
111
"tar",
112
"--extract",
113
"--preserve-permissions",
114
)
115
tarcmd.Dir = dst
116
tarcmd.Stdin = teeReader
117
118
var msg []byte
119
msg, err = tarcmd.CombinedOutput()
120
if err != nil {
121
return xerrors.Errorf("tar %s: %s", dst, err.Error()+";"+string(msg))
122
}
123
124
log.WithField("log", string(msg)).Debug("decompressing tar stream log")
125
126
<-finished
127
tracing.FinishSpan(unpackSpan, &err)
128
129
chownSpan := opentracing.StartSpan("chown", opentracing.ChildOf(span.Context()))
130
// lets create a sorted list of pathes and chown depth first.
131
paths := make([]string, 0, len(m))
132
for path := range m {
133
paths = append(paths, path)
134
}
135
sort.Sort(sort.Reverse(sort.StringSlice(paths)))
136
137
// We need to remap the UID and GID between the host and the container to avoid permission issues.
138
for _, p := range paths {
139
v := m[p]
140
141
if v.IsSymlink {
142
continue
143
}
144
145
uid := toHostID(v.UID, cfg.UIDMaps)
146
gid := toHostID(v.GID, cfg.GIDMaps)
147
148
err = remapFile(path.Join(dst, p), uid, gid, v.Xattrs)
149
if err != nil {
150
log.WithError(err).WithField("uid", uid).WithField("gid", gid).WithField("path", p).Debug("cannot chown")
151
}
152
}
153
tracing.FinishSpan(chownSpan, &err)
154
155
log.WithField("duration", time.Since(start).Milliseconds()).Debug("untar complete")
156
return nil
157
}
158
159
func toHostID(containerID int, idMap []IDMapping) int {
160
for _, m := range idMap {
161
if (containerID >= m.ContainerID) && (containerID <= (m.ContainerID + m.Size - 1)) {
162
hostID := m.HostID + (containerID - m.ContainerID)
163
return hostID
164
}
165
}
166
return containerID
167
}
168
169
// remapFile changes the UID and GID of a file preserving existing file mode bits.
170
func remapFile(name string, uid, gid int, xattrs map[string]string) error {
171
// current info of the file before any change
172
fileInfo, err := os.Stat(name)
173
if err != nil {
174
return err
175
}
176
177
// nothing to do for symlinks
178
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
179
return nil
180
}
181
182
// changing UID or GID can break files with suid/sgid
183
err = os.Lchown(name, uid, gid)
184
if err != nil {
185
return err
186
}
187
188
// restore original permissions
189
err = os.Chmod(name, fileInfo.Mode())
190
if err != nil {
191
return err
192
}
193
194
for key, value := range xattrs {
195
// do not set trusted attributes
196
if strings.HasPrefix(key, "trusted.") {
197
continue
198
}
199
200
if strings.HasPrefix(key, "user.") {
201
// This is a marker to match inodes, such as when an upper layer copies a lower layer file in overlayfs.
202
// However, when restoring a content, the container in the workspace is not always running, so there is no problem ignoring the failure.
203
if strings.HasSuffix(key, ".overlay.impure") || strings.HasSuffix(key, ".overlay.origin") {
204
continue
205
}
206
}
207
208
if err := unix.Lsetxattr(name, key, []byte(value), 0); err != nil {
209
if err == syscall.ENOTSUP || err == syscall.EPERM {
210
continue
211
}
212
213
log.WithField("name", key).WithField("value", value).WithField("file", name).WithError(err).Warn("restoring extended attributes")
214
}
215
}
216
217
// restore file times
218
fileTime := fileInfo.Sys().(*syscall.Stat_t)
219
return os.Chtimes(name, timespecToTime(fileTime.Atim), timespecToTime(fileTime.Mtim))
220
}
221
222
func timespecToTime(ts syscall.Timespec) time.Time {
223
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
224
}
225
226