1package preview
2
3import (
4 "context"
5 "fmt"
6 "strings"
7
8 "github.com/bnema/gordon/internal/domain"
9)
10
11// VolumeCloner abstracts container runtime operations needed for volume cloning.
12type VolumeCloner interface {
13 CreateVolume(ctx context.Context, name string) error
14 RemoveVolume(ctx context.Context, name string, force bool) error
15 CreateContainer(ctx context.Context, config *domain.ContainerConfig) (*domain.Container, error)
16 StartContainer(ctx context.Context, containerID string) error
17 WaitForContainer(ctx context.Context, containerID string) error
18 RemoveContainer(ctx context.Context, containerID string, force bool) error
19 ListVolumes(ctx context.Context) ([]*domain.VolumeInfo, error)
20}
21
22// VolumeNamer is a function that maps a source volume name to a destination volume name.
23type VolumeNamer func(sourceVolName string) string
24
25// DefaultNamer returns a VolumeNamer that uses BuildCloneVolumeName with the given preview name.
26func DefaultNamer(previewName string) VolumeNamer {
27 return func(sourceVolName string) string {
28 return BuildCloneVolumeName(previewName, sourceVolName)
29 }
30}
31
32// BuildCloneVolumeName generates a preview volume name.
33func BuildCloneVolumeName(previewName, volumeName string) string {
34 return "preview-" + previewName + "-" + volumeName
35}
36
37// BuildCloneContainerName generates a preview attachment container name from an image reference.
38func BuildCloneContainerName(previewName, image string) string {
39 name := image
40 if i := strings.LastIndex(name, "/"); i >= 0 {
41 name = name[i+1:]
42 }
43 if i := strings.Index(name, ":"); i >= 0 {
44 name = name[:i]
45 }
46 return "preview-" + previewName + "-" + name
47}
48
49// CloneVolumes copies volumes from source using a read-only helper container.
50// sourceVolumes is a list of actual Docker volume names to clone.
51func CloneVolumes(ctx context.Context, cloner VolumeCloner, namer VolumeNamer, sourceVolumes []string) ([]string, error) {
52 var clonedNames []string
53 for _, sourceVolName := range sourceVolumes {
54 destName := namer(sourceVolName)
55
56 if err := cloner.CreateVolume(ctx, destName); err != nil {
57 cleanupVolumes(ctx, cloner, clonedNames)
58 return nil, fmt.Errorf("create volume %s: %w", destName, err)
59 }
60
61 helperConfig := &domain.ContainerConfig{
62 Image: "busybox:1.37",
63 Name: "gordon-vol-copy-" + destName,
64 Cmd: []string{"cp", "-a", "/src/.", "/dst/"},
65 Volumes: map[string]string{
66 "/dst": destName,
67 },
68 ReadOnlyVolumes: map[string]string{
69 "/src": sourceVolName,
70 },
71 }
72
73 created, err := cloner.CreateContainer(ctx, helperConfig)
74 if err != nil {
75 _ = cloner.RemoveVolume(ctx, destName, true)
76 cleanupVolumes(ctx, cloner, clonedNames)
77 return nil, fmt.Errorf("create copy helper for %s: %w", destName, err)
78 }
79
80 if err := cloner.StartContainer(ctx, created.ID); err != nil {
81 _ = cloner.RemoveContainer(ctx, created.ID, true)
82 _ = cloner.RemoveVolume(ctx, destName, true)
83 cleanupVolumes(ctx, cloner, clonedNames)
84 return nil, fmt.Errorf("start copy helper for %s: %w", destName, err)
85 }
86
87 if err := cloner.WaitForContainer(ctx, created.ID); err != nil {
88 _ = cloner.RemoveContainer(ctx, created.ID, true)
89 _ = cloner.RemoveVolume(ctx, destName, true)
90 cleanupVolumes(ctx, cloner, clonedNames)
91 return nil, fmt.Errorf("wait for copy helper %s: %w", destName, err)
92 }
93
94 _ = cloner.RemoveContainer(ctx, created.ID, true)
95 clonedNames = append(clonedNames, destName)
96 }
97 return clonedNames, nil
98}
99
100func cleanupVolumes(ctx context.Context, cloner VolumeCloner, names []string) {
101 for _, name := range names {
102 _ = cloner.RemoveVolume(ctx, name, true)
103 }
104}