volume_archive_integration_test.go RawHistoryFolder

1//go:build integration
2
3package docker
4
5import (
6	"archive/tar"
7	"compress/gzip"
8	"context"
9	"crypto/sha256"
10	"encoding/hex"
11	"fmt"
12	"io"
13	"os/exec"
14	"strings"
15	"testing"
16	"time"
17
18	"github.com/stretchr/testify/assert"
19	"github.com/stretchr/testify/require"
20
21	"github.com/bnema/gordon/internal/domain"
22)
23
24const volumeArchiveTestImage = "alpine:3.20"
25
26// TestRuntime_ExportVolumeArchive_Integration_BinaryRoundTrip proves the
27// exported stream is a byte-exact archive: binary content (random data
28// that is not valid UTF-8) must survive export unchanged.
29func TestRuntime_ExportVolumeArchive_Integration_BinaryRoundTrip(t *testing.T) {
30	if testing.Short() {
31		t.Skip("skipping integration test in short mode")
32	}
33	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
34	defer cancel()
35
36	volume := fmt.Sprintf("gordon-archive-it-%d", time.Now().UnixNano())
37	t.Cleanup(func() { _ = exec.Command("docker", "volume", "rm", "-f", volume).Run() })
38
39	out, err := exec.CommandContext(ctx, "docker", "run", "--rm", "-v", volume+":/v", volumeArchiveTestImage, "sh", "-c",
40		"head -c 5000000 /dev/urandom > /v/blob && sha256sum /v/blob").CombinedOutput()
41	require.NoError(t, err, string(out))
42	wantSum := strings.Fields(string(out))[0]
43
44	runtime, err := NewRuntime()
45	require.NoError(t, err)
46
47	res, err := runtime.ExportVolumeArchive(ctx, domain.VolumeArchiveRequest{
48		VolumeName:  volume,
49		Compression: domain.VolumeBackupCompressionGzip,
50		HelperImage: volumeArchiveTestImage,
51	})
52	require.NoError(t, err)
53	defer res.Stream.Close()
54
55	gz, err := gzip.NewReader(res.Stream)
56	require.NoError(t, err)
57	tr := tar.NewReader(gz)
58
59	found := false
60	for {
61		header, err := tr.Next()
62		if err == io.EOF {
63			break
64		}
65		require.NoError(t, err)
66		if strings.TrimPrefix(header.Name, "./") != "blob" {
67			continue
68		}
69		hash := sha256.New()
70		_, err = io.Copy(hash, tr)
71		require.NoError(t, err)
72		assert.Equal(t, wantSum, hex.EncodeToString(hash.Sum(nil)))
73		found = true
74	}
75	assert.True(t, found, "archive must contain the blob file")
76}
77
78// TestRuntime_ExportVolumeArchive_Integration_CancelUnblocksStream proves a
79// stalled export cannot outlive its context: once the reader stops, the
80// helper blocks on its output; cancellation must then fail the stream and
81// remove the helper container.
82func TestRuntime_ExportVolumeArchive_Integration_CancelUnblocksStream(t *testing.T) {
83	if testing.Short() {
84		t.Skip("skipping integration test in short mode")
85	}
86	volume := fmt.Sprintf("gordon-archive-it-cancel-%d", time.Now().UnixNano())
87	t.Cleanup(func() { _ = exec.Command("docker", "volume", "rm", "-f", volume).Run() })
88	// Incompressible data far larger than the attach socket buffers.
89	out, err := exec.Command("docker", "run", "--rm", "-v", volume+":/v", volumeArchiveTestImage, "sh", "-c",
90		"head -c 30000000 /dev/urandom > /v/blob").CombinedOutput()
91	require.NoError(t, err, string(out))
92
93	runtime, err := NewRuntime()
94	require.NoError(t, err)
95
96	ctx, cancel := context.WithCancel(context.Background())
97	defer cancel()
98	res, err := runtime.ExportVolumeArchive(ctx, domain.VolumeArchiveRequest{
99		VolumeName:  volume,
100		Compression: domain.VolumeBackupCompressionGzip,
101		HelperImage: volumeArchiveTestImage,
102	})
103	require.NoError(t, err)
104	defer res.Stream.Close()
105
106	// Read a little, then stop consuming so the helper blocks on output.
107	_, err = io.ReadFull(res.Stream, make([]byte, 1024))
108	require.NoError(t, err)
109	cancel()
110
111	// Without reading further, cancellation alone must tear the export down.
112	require.Eventually(t, func() bool {
113		ps, psErr := exec.Command("docker", "ps", "-aq", "--filter", "volume="+volume).Output()
114		return psErr == nil && strings.TrimSpace(string(ps)) == ""
115	}, 30*time.Second, 200*time.Millisecond, "helper container must be removed after cancellation")
116
117	_, err = io.ReadAll(res.Stream)
118	require.Error(t, err, "a canceled export must not end as a complete archive")
119}
120
121// TestRuntime_ExportVolumeArchive_Integration_HelperFailure proves a helper
122// that exits non-zero fails the stream instead of producing an artifact.
123func TestRuntime_ExportVolumeArchive_Integration_HelperFailure(t *testing.T) {
124	if testing.Short() {
125		t.Skip("skipping integration test in short mode")
126	}
127	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
128	defer cancel()
129
130	runtime, err := NewRuntime()
131	require.NoError(t, err)
132
133	// Docker creates the missing volume on mount; remove it afterwards.
134	volume := fmt.Sprintf("gordon-archive-it-fail-%d", time.Now().UnixNano())
135	t.Cleanup(func() { _ = exec.Command("docker", "volume", "rm", "-f", volume).Run() })
136
137	// zstd is absent from plain alpine, so the helper exits non-zero.
138	res, err := runtime.ExportVolumeArchive(ctx, domain.VolumeArchiveRequest{
139		VolumeName:  volume,
140		Compression: domain.VolumeBackupCompressionZstd,
141		HelperImage: volumeArchiveTestImage,
142	})
143	require.NoError(t, err)
144	defer res.Stream.Close()
145
146	_, err = io.ReadAll(res.Stream)
147	require.Error(t, err)
148	assert.Contains(t, err.Error(), "volume archive helper exited with code")
149}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close