Skip to content

Commit 55dad24

Browse files
committed
[content-init] apply UID mapping symmetrically
1 parent 3644d93 commit 55dad24

File tree

26 files changed

+498
-115
lines changed

26 files changed

+498
-115
lines changed

components/content-service/go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ replace github.com/Sirupsen/logrus v1.6.0 => github.com/sirupsen/logrus v1.6.0 /
1111

1212
require (
1313
cloud.google.com/go/storage v1.10.0
14+
github.com/Microsoft/go-winio v0.4.16 // indirect
15+
github.com/Microsoft/hcsshim v0.8.14 // indirect
16+
github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7 // indirect
17+
github.com/docker/docker v1.13.1
18+
github.com/docker/go-units v0.4.0 // indirect
1419
github.com/gitpod-io/gitpod/common-go v0.0.0-00010101000000-000000000000
1520
github.com/gitpod-io/gitpod/content-service/api v0.0.0-00010101000000-000000000000
1621
github.com/go-ozzo/ozzo-validation v3.5.0+incompatible
@@ -22,6 +27,7 @@ require (
2227
github.com/minio/minio-go/v7 v7.0.7
2328
github.com/opencontainers/go-digest v1.0.0
2429
github.com/opencontainers/image-spec v1.0.1
30+
github.com/opencontainers/runc v0.1.1 // indirect
2531
github.com/opentracing/opentracing-go v1.1.0
2632
github.com/prometheus/client_golang v1.1.0
2733
github.com/spf13/cobra v1.1.1
@@ -32,6 +38,7 @@ require (
3238
golang.org/x/text v0.3.5 // indirect
3339
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
3440
google.golang.org/api v0.32.0
41+
google.golang.org/appengine v1.6.6
3542
google.golang.org/grpc v1.34.0
3643
google.golang.org/protobuf v1.25.0
3744
gopkg.in/ini.v1 v1.62.0 // indirect

components/content-service/go.sum

Lines changed: 44 additions & 21 deletions
Large diffs are not rendered by default.

components/ws-daemon/pkg/archive/tar.go renamed to components/content-service/pkg/archive/tar.go

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,37 @@
55
package archive
66

77
import (
8+
"archive/tar"
89
"bufio"
910
"context"
1011
"fmt"
1112
"io"
1213
"os"
14+
"os/exec"
15+
"path"
16+
"sort"
17+
"time"
1318

1419
"github.com/docker/docker/pkg/archive"
1520
"github.com/docker/docker/pkg/idtools"
21+
"github.com/gitpod-io/gitpod/common-go/log"
1622
"github.com/gitpod-io/gitpod/common-go/tracing"
1723
"github.com/opentracing/opentracing-go"
1824
"golang.org/x/xerrors"
1925
)
2026

21-
type buildTarbalConfig struct {
27+
type tarConfig struct {
2228
MaxSizeBytes int64
2329
UIDMaps []idtools.IDMap
2430
GIDMaps []idtools.IDMap
2531
}
2632

2733
// BuildTarbalOption configures the tarbal creation
28-
type BuildTarbalOption func(o *buildTarbalConfig)
34+
type TarOption func(o *tarConfig)
2935

3036
// TarbalMaxSize limits the size of a tarbal
31-
func TarbalMaxSize(n int64) BuildTarbalOption {
32-
return func(o *buildTarbalConfig) {
37+
func TarbalMaxSize(n int64) TarOption {
38+
return func(o *tarConfig) {
3339
o.MaxSizeBytes = n
3440
}
3541
}
@@ -42,8 +48,8 @@ type IDMapping struct {
4248
}
4349

4450
// WithUIDMapping reverses the given user ID mapping during archive creation
45-
func WithUIDMapping(mappings []IDMapping) BuildTarbalOption {
46-
return func(o *buildTarbalConfig) {
51+
func WithUIDMapping(mappings []IDMapping) TarOption {
52+
return func(o *tarConfig) {
4753
o.UIDMaps = make([]idtools.IDMap, len(mappings))
4854
for i, m := range mappings {
4955
o.UIDMaps[i] = idtools.IDMap{
@@ -56,8 +62,8 @@ func WithUIDMapping(mappings []IDMapping) BuildTarbalOption {
5662
}
5763

5864
// WithGIDMapping reverses the given user ID mapping during archive creation
59-
func WithGIDMapping(mappings []IDMapping) BuildTarbalOption {
60-
return func(o *buildTarbalConfig) {
65+
func WithGIDMapping(mappings []IDMapping) TarOption {
66+
return func(o *tarConfig) {
6167
o.GIDMaps = make([]idtools.IDMap, len(mappings))
6268
for i, m := range mappings {
6369
o.GIDMaps[i] = idtools.IDMap{
@@ -69,9 +75,89 @@ func WithGIDMapping(mappings []IDMapping) BuildTarbalOption {
6975
}
7076
}
7177

78+
// ExtractTarbal extracts an OCI compatible tar file src to the folder dst, expecting the overlay whiteout format
79+
func ExtractTarbal(ctx context.Context, src io.Reader, dst string, opts ...TarOption) (err error) {
80+
var cfg tarConfig
81+
start := time.Now()
82+
for _, opt := range opts {
83+
opt(&cfg)
84+
}
85+
86+
//nolint:staticcheck,ineffassign
87+
span, ctx := opentracing.StartSpanFromContext(ctx, "extractTarbal")
88+
span.LogKV("src", src, "dst", dst)
89+
defer tracing.FinishSpan(span, &err)
90+
91+
pr, pw := io.Pipe()
92+
src = io.TeeReader(src, pw)
93+
tarReader := tar.NewReader(pr)
94+
type Info struct {
95+
UID, GID int
96+
}
97+
finished := make(chan bool)
98+
m := make(map[string]Info)
99+
go func() {
100+
defer close(finished)
101+
for {
102+
hdr, err := tarReader.Next()
103+
if err == io.EOF {
104+
finished <- true
105+
return
106+
}
107+
if err != nil {
108+
log.WithError(err).Error("error reading tar")
109+
return
110+
} else {
111+
m[hdr.Name] = Info{
112+
UID: hdr.Uid,
113+
GID: hdr.Gid,
114+
}
115+
}
116+
}
117+
}()
118+
119+
tarcmd := exec.Command("tar", "x")
120+
tarcmd.Dir = dst
121+
tarcmd.Stdin = src
122+
123+
msg, err := tarcmd.CombinedOutput()
124+
if err != nil {
125+
return xerrors.Errorf("tar %s: %s", dst, err.Error()+";"+string(msg))
126+
}
127+
<-finished
128+
129+
// lets create a sorted list of pathes and chown depth first.
130+
paths := make([]string, 0, len(m))
131+
for path := range m {
132+
paths = append(paths, path)
133+
}
134+
sort.Sort(sort.Reverse(sort.StringSlice(paths)))
135+
for _, p := range paths {
136+
v := m[p]
137+
uid := toHostID(v.UID, cfg.UIDMaps)
138+
gid := toHostID(v.GID, cfg.GIDMaps)
139+
err = os.Lchown(path.Join(dst, p), uid, gid)
140+
if err != nil {
141+
log.WithError(err).WithField("uid", uid).WithField("gid", gid).WithField("path", p).Warn("cannot chown")
142+
}
143+
}
144+
log.WithField("duration", time.Since(start).Milliseconds()).Debug("untar complete")
145+
return nil
146+
}
147+
148+
func toHostID(containerID int, idMap []idtools.IDMap) int {
149+
for _, m := range idMap {
150+
if (containerID >= m.ContainerID) && (containerID <= (m.ContainerID + m.Size - 1)) {
151+
hostID := m.HostID + (containerID - m.ContainerID)
152+
return hostID
153+
}
154+
}
155+
return containerID
156+
}
157+
72158
// BuildTarbal creates an OCI compatible tar file dst from the folder src, expecting the overlay whiteout format
73-
func BuildTarbal(ctx context.Context, src string, dst string, opts ...BuildTarbalOption) (err error) {
74-
var cfg buildTarbalConfig
159+
func BuildTarbal(ctx context.Context, src string, dst string, opts ...TarOption) (err error) {
160+
var cfg tarConfig
75161
for _, opt := range opts {
76162
opt(&cfg)
77163
}

components/ws-daemon/pkg/archive/tar_test.go renamed to components/content-service/pkg/archive/tar_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io/ioutil"
1010
"os"
1111
"path/filepath"
12+
"syscall"
1213
"testing"
1314
)
1415

@@ -94,3 +95,81 @@ func TestBuildTarbalMaxSize(t *testing.T) {
9495
os.RemoveAll(c)
9596
}
9697
}
98+
99+
func TestExtractTarbal(t *testing.T) {
100+
type file = struct {
101+
Name string
102+
ContentSize int64
103+
Uid int
104+
}
105+
tests := []struct {
106+
Name string
107+
Files []file
108+
}{
109+
{
110+
Name: "simple-test",
111+
Files: []file{
112+
{"file.txt", 1024, 33333},
113+
{"file2.txt", 1024, 33333},
114+
},
115+
},
116+
{
117+
Name: "empty-tar",
118+
Files: []file{},
119+
},
120+
}
121+
122+
for _, test := range tests {
123+
t.Run(test.Name, func(t *testing.T) {
124+
wd, err := ioutil.TempDir("", "")
125+
defer os.RemoveAll(wd)
126+
if err != nil {
127+
t.Errorf("cannot prepare test: %v", err)
128+
t.FailNow()
129+
}
130+
sourceFolder := filepath.Join(wd, "source")
131+
os.MkdirAll(sourceFolder, 0777)
132+
133+
for _, file := range test.Files {
134+
fileName := filepath.Join(sourceFolder, file.Name)
135+
err = ioutil.WriteFile(fileName, make([]byte, file.ContentSize), 0644)
136+
if err != nil {
137+
t.Errorf("cannot prepare test: %v", err)
138+
continue
139+
}
140+
err = os.Chown(fileName, file.Uid, file.Uid)
141+
if err != nil {
142+
t.Errorf("Cannot chown %s to %d: %s", file.Name, file.Uid, err)
143+
}
144+
}
145+
tarFile := filepath.Join(wd, "my.tar")
146+
BuildTarbal(context.Background(), sourceFolder, tarFile)
147+
148+
reader, err := os.Open(tarFile)
149+
if err != nil {
150+
t.Errorf("Cannot open %s", tarFile)
151+
}
152+
targetFolder := filepath.Join(wd, "target")
153+
os.MkdirAll(targetFolder, 0777)
154+
ExtractTarbal(context.Background(), reader, targetFolder)
155+
156+
for _, file := range test.Files {
157+
stat, err := os.Stat(filepath.Join(targetFolder, file.Name))
158+
if err != nil {
159+
t.Errorf("expected %s", file.Name)
160+
continue
161+
}
162+
uid := stat.Sys().(*syscall.Stat_t).Uid
163+
if uid != uint32(file.Uid) {
164+
t.Errorf("expected uid %d", file.Uid)
165+
continue
166+
}
167+
gid := stat.Sys().(*syscall.Stat_t).Gid
168+
if gid != uint32(file.Uid) {
169+
t.Errorf("expected gid %d", file.Uid)
170+
continue
171+
}
172+
}
173+
})
174+
}
175+
}

components/content-service/pkg/initializer/git.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/gitpod-io/gitpod/common-go/log"
1414
"github.com/gitpod-io/gitpod/common-go/tracing"
1515
csapi "github.com/gitpod-io/gitpod/content-service/api"
16+
"github.com/gitpod-io/gitpod/content-service/pkg/archive"
1617
"github.com/gitpod-io/gitpod/content-service/pkg/git"
1718
"github.com/opentracing/opentracing-go"
1819
)
@@ -46,7 +47,7 @@ type GitInitializer struct {
4647
}
4748

4849
// Run initializes the workspace using Git
49-
func (ws *GitInitializer) Run(ctx context.Context) (src csapi.WorkspaceInitSource, err error) {
50+
func (ws *GitInitializer) Run(ctx context.Context, mappings []archive.IDMapping) (src csapi.WorkspaceInitSource, err error) {
5051
isGitWS := git.IsWorkingCopy(ws.Location)
5152

5253
span, ctx := opentracing.StartSpanFromContext(ctx, "GitInitializer.Run")

components/content-service/pkg/initializer/initializer.go

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import (
1111
"io/ioutil"
1212
"net/http"
1313
"os"
14-
"os/exec"
1514
"path/filepath"
1615
"strings"
1716
"time"
1817

1918
"github.com/gitpod-io/gitpod/common-go/log"
2019
"github.com/gitpod-io/gitpod/common-go/tracing"
2120
csapi "github.com/gitpod-io/gitpod/content-service/api"
21+
"github.com/gitpod-io/gitpod/content-service/pkg/archive"
2222
"github.com/gitpod-io/gitpod/content-service/pkg/git"
2323
"github.com/gitpod-io/gitpod/content-service/pkg/storage"
2424
"github.com/opentracing/opentracing-go"
@@ -43,30 +43,17 @@ const (
4343

4444
// Initializer can initialize a workspace with content
4545
type Initializer interface {
46-
Run(ctx context.Context) (csapi.WorkspaceInitSource, error)
46+
Run(ctx context.Context, mappings []archive.IDMapping) (csapi.WorkspaceInitSource, error)
4747
}
4848

4949
// EmptyInitializer does nothing
5050
type EmptyInitializer struct{}
5151

5252
// Run does nothing
53-
func (e *EmptyInitializer) Run(ctx context.Context) (csapi.WorkspaceInitSource, error) {
53+
func (e *EmptyInitializer) Run(ctx context.Context, mappings []archive.IDMapping) (csapi.WorkspaceInitSource, error) {
5454
return csapi.WorkspaceInitFromOther, nil
5555
}
5656

57-
// recursiveChown chown's the location and all its childen to
58-
func recursiveChown(ctx context.Context, location string, uid, gid int) (err error) {
59-
span, _ := opentracing.StartSpanFromContext(ctx, "recursiveChown")
60-
defer tracing.FinishSpan(span, &err)
61-
62-
out, err := exec.Command("chown", "-R", fmt.Sprintf("%d:%d", uid, gid), location).CombinedOutput()
63-
if err != nil {
64-
return xerrors.Errorf("chown -R %s: %s", location, out)
65-
}
66-
67-
return nil
68-
}
69-
7057
// NewFromRequest picks the initializer from the request but does not execute it.
7158
// Returns gRPC errors.
7259
func NewFromRequest(ctx context.Context, loc string, rs storage.DirectDownloader, req *csapi.WorkspaceInitializer) (i Initializer, err error) {
@@ -254,6 +241,14 @@ type initializeOpts struct {
254241
InWorkspace bool
255242
UID int
256243
GID int
244+
mappings []archive.IDMapping
245+
}
246+
247+
// WithMappings configures the UID mappings that're used during content initialization
248+
func WithMappings(mappings []archive.IDMapping) InitializeOpt {
249+
return func(o *initializeOpts) {
250+
o.mappings = mappings
251+
}
257252
}
258253

259254
// WithInitializer configures the initializer that's used during content initialization
@@ -331,7 +326,7 @@ func InitializeWorkspace(ctx context.Context, location string, remoteStorage sto
331326
}
332327

333328
// Run the initializer
334-
hasBackup, err := remoteStorage.Download(ctx, location, storage.DefaultBackup)
329+
hasBackup, err := remoteStorage.Download(ctx, location, storage.DefaultBackup, cfg.mappings)
335330
if err != nil {
336331
return src, xerrors.Errorf("cannot restore backup: %w", err)
337332
}
@@ -340,19 +335,12 @@ func InitializeWorkspace(ctx context.Context, location string, remoteStorage sto
340335
if hasBackup {
341336
src = csapi.WorkspaceInitFromBackup
342337
} else {
343-
src, err = cfg.Initializer.Run(ctx)
338+
src, err = cfg.Initializer.Run(ctx, cfg.mappings)
344339
if err != nil {
345340
return src, xerrors.Errorf("cannot initialize workspace: %w", err)
346341
}
347342
}
348343

349-
if !cfg.InWorkspace {
350-
err = recursiveChown(ctx, location, cfg.UID, cfg.GID)
351-
if err != nil {
352-
return src, xerrors.Errorf("cannot set workspace permissions: %w", err)
353-
}
354-
}
355-
356344
return
357345
}
358346

0 commit comments

Comments
 (0)