Skip to content

Commit 08acca8

Browse files
sagor999roboquat
authored andcommitted
[content-service] add prestop hook to extract git status when workspace is using pvc
1 parent 98726d3 commit 08acca8

File tree

5 files changed

+332
-5
lines changed

5 files changed

+332
-5
lines changed

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,54 @@ func (c *Client) Git(ctx context.Context, subcommand string, args ...string) (er
215215
return nil
216216
}
217217

218+
// GitStatusFromFiles same as Status but reads git output from preexisting files that were generated by prestop hook
219+
func GitStatusFromFiles(ctx context.Context, loc string) (res *Status, err error) {
220+
gitout, err := os.ReadFile(filepath.Join(loc, "git_status.txt"))
221+
if err != nil {
222+
return nil, err
223+
}
224+
porcelain, err := parsePorcelain(bytes.NewReader(gitout))
225+
if err != nil {
226+
return nil, err
227+
}
228+
229+
unpushedCommits := make([]string, 0)
230+
gitout, err = os.ReadFile(filepath.Join(loc, "git_log_1.txt"))
231+
if err != nil && !strings.Contains(err.Error(), errNoCommitsYet) {
232+
return nil, err
233+
}
234+
if gitout != nil {
235+
out, err := io.ReadAll(bytes.NewReader(gitout))
236+
if err != nil {
237+
return nil, xerrors.Errorf("cannot determine unpushed commits: %w", err)
238+
}
239+
for _, l := range strings.Split(string(out), "\n") {
240+
tl := strings.TrimSpace(l)
241+
if tl != "" {
242+
unpushedCommits = append(unpushedCommits, tl)
243+
}
244+
}
245+
}
246+
if len(unpushedCommits) == 0 {
247+
unpushedCommits = nil
248+
}
249+
250+
latestCommit := ""
251+
gitout, err = os.ReadFile(filepath.Join(loc, "git_log_2.txt"))
252+
if err != nil && !strings.Contains(err.Error(), errNoCommitsYet) {
253+
return nil, err
254+
}
255+
if len(gitout) > 0 {
256+
latestCommit = strings.TrimSpace(string(gitout))
257+
}
258+
259+
return &Status{
260+
porcelainStatus: *porcelain,
261+
UnpushedCommits: unpushedCommits,
262+
LatestCommit: latestCommit,
263+
}, nil
264+
}
265+
218266
// Status runs git status
219267
func (c *Client) Status(ctx context.Context) (res *Status, err error) {
220268
gitout, err := c.GitWithOutput(ctx, "status", "--porcelain=v2", "--branch", "-uall")

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

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"os"
1010
"path/filepath"
11+
"strings"
1112
"testing"
1213
"time"
1314

@@ -228,6 +229,246 @@ func TestGitStatus(t *testing.T) {
228229
}
229230
}
230231

232+
func TestGitStatusFromFiles(t *testing.T) {
233+
tests := []struct {
234+
Name string
235+
Prep func(context.Context, *Client) error
236+
Result *Status
237+
Error error
238+
}{
239+
{
240+
"no commits",
241+
func(ctx context.Context, c *Client) error {
242+
if err := c.Git(ctx, "init"); err != nil {
243+
return err
244+
}
245+
return nil
246+
},
247+
&Status{
248+
porcelainStatus: porcelainStatus{
249+
BranchOID: "(initial)",
250+
BranchHead: "master",
251+
},
252+
},
253+
nil,
254+
},
255+
{
256+
"clean copy",
257+
func(ctx context.Context, c *Client) error {
258+
if err := initFromRemote(ctx, c); err != nil {
259+
return err
260+
}
261+
return nil
262+
},
263+
&Status{
264+
porcelainStatus: porcelainStatus{
265+
BranchHead: "master",
266+
BranchOID: notEmpty,
267+
},
268+
LatestCommit: notEmpty,
269+
},
270+
nil,
271+
},
272+
{
273+
"untracked files",
274+
func(ctx context.Context, c *Client) error {
275+
if err := initFromRemote(ctx, c); err != nil {
276+
return err
277+
}
278+
if err := os.WriteFile(filepath.Join(c.Location, "another-file"), []byte{}, 0755); err != nil {
279+
return err
280+
}
281+
return nil
282+
},
283+
&Status{
284+
porcelainStatus: porcelainStatus{
285+
BranchHead: "master",
286+
BranchOID: notEmpty,
287+
UntrackedFiles: []string{"another-file"},
288+
},
289+
LatestCommit: notEmpty,
290+
},
291+
nil,
292+
},
293+
{
294+
"uncommitted files",
295+
func(ctx context.Context, c *Client) error {
296+
if err := initFromRemote(ctx, c); err != nil {
297+
return err
298+
}
299+
if err := os.WriteFile(filepath.Join(c.Location, "first-file"), []byte("foobar"), 0755); err != nil {
300+
return err
301+
}
302+
return nil
303+
},
304+
&Status{
305+
porcelainStatus: porcelainStatus{
306+
BranchHead: "master",
307+
BranchOID: notEmpty,
308+
UncommitedFiles: []string{"first-file"},
309+
},
310+
LatestCommit: notEmpty,
311+
},
312+
nil,
313+
},
314+
{
315+
"unpushed commits",
316+
func(ctx context.Context, c *Client) error {
317+
if err := initFromRemote(ctx, c); err != nil {
318+
return err
319+
}
320+
if err := os.WriteFile(filepath.Join(c.Location, "first-file"), []byte("foobar"), 0755); err != nil {
321+
return err
322+
}
323+
if err := c.Git(ctx, "commit", "-a", "-m", "foo"); err != nil {
324+
return err
325+
}
326+
return nil
327+
},
328+
&Status{
329+
porcelainStatus: porcelainStatus{
330+
BranchHead: "master",
331+
BranchOID: notEmpty,
332+
},
333+
UnpushedCommits: []string{notEmpty},
334+
LatestCommit: notEmpty,
335+
},
336+
nil,
337+
},
338+
{
339+
"unpushed commits in new branch",
340+
func(ctx context.Context, c *Client) error {
341+
if err := initFromRemote(ctx, c); err != nil {
342+
return err
343+
}
344+
if err := c.Git(ctx, "checkout", "-b", "otherbranch"); err != nil {
345+
return err
346+
}
347+
if err := os.WriteFile(filepath.Join(c.Location, "first-file"), []byte("foobar"), 0755); err != nil {
348+
return err
349+
}
350+
if err := c.Git(ctx, "commit", "-a", "-m", "foo"); err != nil {
351+
return err
352+
}
353+
return nil
354+
},
355+
&Status{
356+
porcelainStatus: porcelainStatus{
357+
BranchHead: "otherbranch",
358+
BranchOID: notEmpty,
359+
},
360+
UnpushedCommits: []string{notEmpty},
361+
LatestCommit: notEmpty,
362+
},
363+
nil,
364+
},
365+
366+
{
367+
"pending in sub-dir files",
368+
func(ctx context.Context, c *Client) error {
369+
if err := initFromRemote(ctx, c); err != nil {
370+
return err
371+
}
372+
if err := os.MkdirAll(filepath.Join(c.Location, "this/is/a/nested/test"), 0755); err != nil {
373+
return err
374+
}
375+
if err := os.WriteFile(filepath.Join(c.Location, "this/is/a/nested/test/first-file"), []byte("foobar"), 0755); err != nil {
376+
return err
377+
}
378+
return nil
379+
},
380+
&Status{
381+
porcelainStatus: porcelainStatus{
382+
BranchHead: "master",
383+
BranchOID: notEmpty,
384+
UntrackedFiles: []string{"this/is/a/nested/test/first-file"},
385+
},
386+
LatestCommit: notEmpty,
387+
},
388+
nil,
389+
},
390+
}
391+
392+
for _, test := range tests {
393+
t.Run(test.Name, func(t *testing.T) {
394+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
395+
defer cancel()
396+
397+
client, err := newGitClient(ctx)
398+
if err != nil {
399+
t.Errorf("cannot prep %s: %v", test.Name, err)
400+
return
401+
}
402+
403+
err = test.Prep(ctx, client)
404+
if err != nil {
405+
t.Errorf("cannot prep %s: %v", test.Name, err)
406+
return
407+
}
408+
409+
gitout, err := client.GitWithOutput(ctx, "status", "--porcelain=v2", "--branch", "-uall")
410+
if err != nil {
411+
t.Errorf("error calling GitWithOutput: %v", err)
412+
return
413+
}
414+
if err := os.WriteFile(filepath.Join("/tmp", "git_status.txt"), gitout, 0755); err != nil {
415+
t.Errorf("error creating file: %v", err)
416+
return
417+
}
418+
419+
gitout, err = client.GitWithOutput(ctx, "log", "--pretty=%h: %s", "--branches", "--not", "--remotes")
420+
if err != nil {
421+
t.Errorf("error calling GitWithOutput: %v", err)
422+
return
423+
}
424+
if err := os.WriteFile(filepath.Join("/tmp", "git_log_1.txt"), gitout, 0755); err != nil {
425+
t.Errorf("error creating file: %v", err)
426+
return
427+
}
428+
429+
gitout, err = client.GitWithOutput(ctx, "log", "--pretty=%H", "-n", "1")
430+
if err != nil && !strings.Contains(err.Error(), "fatal: your current branch 'master' does not have any commits yet") {
431+
t.Errorf("error calling GitWithOutput: %v", err)
432+
return
433+
}
434+
if err := os.WriteFile(filepath.Join("/tmp", "git_log_2.txt"), gitout, 0755); err != nil {
435+
t.Errorf("error creating file: %v", err)
436+
return
437+
}
438+
439+
status, err := GitStatusFromFiles(ctx, "/tmp")
440+
if err != test.Error {
441+
t.Errorf("expected error does not match for %s: %v != %v", test.Name, err, test.Error)
442+
return
443+
}
444+
445+
if status != nil {
446+
if test.Result.BranchOID == notEmpty && status.LatestCommit != "" {
447+
test.Result.BranchOID = status.LatestCommit
448+
}
449+
if test.Result.LatestCommit == notEmpty && status.LatestCommit != "" {
450+
test.Result.LatestCommit = status.LatestCommit
451+
}
452+
for _, c := range test.Result.UnpushedCommits {
453+
if c == notEmpty {
454+
if len(status.UnpushedCommits) == 0 {
455+
t.Errorf("expected unpushed commits")
456+
}
457+
458+
test.Result.UnpushedCommits = status.UnpushedCommits
459+
break
460+
}
461+
}
462+
}
463+
464+
if diff := cmp.Diff(test.Result, status, cmp.AllowUnexported(Status{})); diff != "" {
465+
t.Errorf("unexpected status (-want +got):\n%s", diff)
466+
}
467+
468+
})
469+
}
470+
}
471+
231472
func newGitClient(ctx context.Context) (*Client, error) {
232473
loc, err := os.MkdirTemp("", "gittest")
233474
if err != nil {

components/content-service/pkg/layer/provider.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -465,15 +465,27 @@ func contentDescriptorToLayer(cdesc []byte) (*Layer, error) {
465465
)
466466
}
467467

468+
var prestophookScript = `#!/bin/bash
469+
cd ${GITPOD_REPO_ROOT}
470+
git config --global --add safe.directory ${GITPOD_REPO_ROOT}
471+
git status --porcelain=v2 --branch -uall > /.workspace/prestophookdata/git_status.txt
472+
git log --pretty='%h: %s' --branches --not --remotes > /.workspace/prestophookdata/git_log_1.txt
473+
git log --pretty=%H -n 1 > /.workspace/prestophookdata/git_log_2.txt
474+
`
475+
468476
// version of this function for persistent volume claim feature
469477
// we cannot use /workspace folder as when mounting /workspace folder through PVC
470478
// it will mask anything that was in container layer, hence we are using /.workspace instead here
471479
func contentDescriptorToLayerPVC(cdesc []byte) (*Layer, error) {
472-
return layerFromContent(
473-
fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/.workspace", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},
474-
fileInLayer{&tar.Header{Typeflag: tar.TypeDir, Name: "/.workspace/.gitpod", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},
475-
fileInLayer{&tar.Header{Typeflag: tar.TypeReg, Name: "/.workspace/.gitpod/content.json", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755, Size: int64(len(cdesc))}, cdesc},
476-
)
480+
layers := []fileInLayer{
481+
{&tar.Header{Typeflag: tar.TypeDir, Name: "/.workspace", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},
482+
{&tar.Header{Typeflag: tar.TypeDir, Name: "/.workspace/.gitpod", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755}, nil},
483+
{&tar.Header{Typeflag: tar.TypeReg, Name: "/.supervisor/prestophook.sh", Uid: 0, Gid: 0, Mode: 0775, Size: int64(len(prestophookScript))}, []byte(prestophookScript)},
484+
}
485+
if len(cdesc) > 0 {
486+
layers = append(layers, fileInLayer{&tar.Header{Typeflag: tar.TypeReg, Name: "/.workspace/.gitpod/content.json", Uid: initializer.GitpodUID, Gid: initializer.GitpodGID, Mode: 0755, Size: int64(len(cdesc))}, cdesc})
487+
}
488+
return layerFromContent(layers...)
477489
}
478490

479491
func workspaceReadyLayer(src csapi.WorkspaceInitSource) (*Layer, error) {

components/ws-daemon/pkg/content/service.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"fmt"
1212
"math"
1313
"os"
14+
"os/exec"
1415
"path/filepath"
1516
"syscall"
1617
"time"
@@ -240,6 +241,22 @@ func (s *WorkspaceService) InitWorkspace(ctx context.Context, req *api.InitWorks
240241
}
241242
}
242243

244+
if req.PersistentVolumeClaim {
245+
// create a folder that is used to store data from running prestophook
246+
deamonDir := fmt.Sprintf("%s-daemon", req.Id)
247+
prestophookDir := filepath.Join(s.config.WorkingArea, deamonDir, "prestophookdata")
248+
err = os.MkdirAll(prestophookDir, 0755)
249+
if err != nil {
250+
log.WithError(err).WithField("workspaceId", req.Id).Error("cannot create prestophookdata folder")
251+
return nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("cannot create prestophookdata: %v", err))
252+
}
253+
_, err = exec.CommandContext(ctx, "chown", "-R", fmt.Sprintf("%d:%d", wsinit.GitpodUID, wsinit.GitpodGID), prestophookDir).CombinedOutput()
254+
if err != nil {
255+
log.WithError(err).WithField("workspaceId", req.Id).Error("cannot chown prestophookdata folder")
256+
return nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("cannot chown prestophookdata: %v", err))
257+
}
258+
}
259+
243260
// Tell the world we're done
244261
err = workspace.MarkInitDone(ctx)
245262
if err != nil {

components/ws-manager/pkg/manager/create.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,15 @@ func (m *Manager) createDefiniteWorkspacePod(startContext *startWorkspaceContext
545545
// not needed, since it is using dedicated disk
546546
pod.Spec.Containers[0].VolumeMounts[0].MountPropagation = nil
547547

548+
// add prestop hook to capture git status
549+
pod.Spec.Containers[0].Lifecycle = &corev1.Lifecycle{
550+
PreStop: &corev1.LifecycleHandler{
551+
Exec: &corev1.ExecAction{
552+
Command: []string{"/bin/sh", "-c", "/.supervisor/workspacekit lift /.supervisor/prestophook.sh"},
553+
},
554+
},
555+
}
556+
548557
// pavel: 133332 is the Gitpod UID (33333) shifted by 99999. The shift happens inside the workspace container due to the user namespace use.
549558
// We set this magical ID to make sure that gitpod user inside the workspace can write into /workspace folder mounted by PVC
550559
gitpodGUID := int64(133332)

0 commit comments

Comments
 (0)