Skip to content

Commit a6b1052

Browse files
committed
Add podman package
The podman package abstract the methods interacting with podman and for extracting the container filesystem and the containers life-cycle. Signed-off-by: Alice Frosi <[email protected]>
1 parent 84376e9 commit a6b1052

File tree

1 file changed

+282
-0
lines changed

1 file changed

+282
-0
lines changed

pkg/podman/podman.go

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package podman
2+
3+
import (
4+
"archive/tar"
5+
"context"
6+
"fmt"
7+
"io"
8+
"os"
9+
"os/user"
10+
"path/filepath"
11+
"time"
12+
13+
"github.com/containers/podman-bootc/pkg/utils"
14+
ocispec "github.com/opencontainers/runtime-spec/specs-go"
15+
log "github.com/sirupsen/logrus"
16+
17+
"github.com/containers/podman/v5/libpod/define"
18+
"github.com/containers/podman/v5/pkg/bindings"
19+
"github.com/containers/podman/v5/pkg/bindings/containers"
20+
"github.com/containers/podman/v5/pkg/specgen"
21+
)
22+
23+
func createContainer(ctx context.Context, vmImage string) (string, error) {
24+
specGen := &specgen.SpecGenerator{
25+
ContainerBasicConfig: specgen.ContainerBasicConfig{
26+
Command: []string{"/"},
27+
},
28+
ContainerStorageConfig: specgen.ContainerStorageConfig{
29+
Image: vmImage,
30+
},
31+
}
32+
if err := specGen.Validate(); err != nil {
33+
return "", err
34+
}
35+
response, err := containers.CreateWithSpec(ctx, specGen, &containers.CreateOptions{})
36+
if err != nil {
37+
return "", err
38+
}
39+
40+
return response.ID, nil
41+
}
42+
43+
func extractTar(reader io.Reader, dest string) error {
44+
tr := tar.NewReader(reader)
45+
46+
for {
47+
header, err := tr.Next()
48+
if err == io.EOF {
49+
break
50+
}
51+
if err != nil {
52+
return err
53+
}
54+
55+
target := filepath.Join(dest, header.Name)
56+
57+
switch header.Typeflag {
58+
case tar.TypeDir:
59+
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
60+
return err
61+
}
62+
case tar.TypeReg:
63+
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
64+
return err
65+
}
66+
outFile, err := os.Create(target)
67+
if err != nil {
68+
return err
69+
}
70+
if _, err := io.Copy(outFile, tr); err != nil {
71+
outFile.Close()
72+
return err
73+
}
74+
outFile.Close()
75+
}
76+
}
77+
78+
return nil
79+
}
80+
81+
func ExtractDiskImage(socketPath, dir, vmImage string) error {
82+
if err := os.Mkdir(dir, 0750); err != nil && !os.IsExist(err) {
83+
return err
84+
}
85+
ctx, err := bindings.NewConnection(context.Background(), fmt.Sprintf("unix:%s", socketPath))
86+
if err != nil {
87+
return err
88+
}
89+
90+
containerName, err := createContainer(ctx, vmImage)
91+
if err != nil {
92+
return err
93+
}
94+
95+
pr, pw := io.Pipe()
96+
97+
go func() {
98+
defer pw.Close()
99+
err := containers.Export(ctx, containerName, pw, &containers.ExportOptions{})
100+
if err != nil {
101+
// If an error occurs, propagate it to the pipe reader
102+
pw.CloseWithError(err)
103+
}
104+
}()
105+
if err := extractTar(pr, dir); err != nil {
106+
return err
107+
}
108+
log.Debugf("Extracted disk at: %s", dir)
109+
110+
return nil
111+
}
112+
113+
func connectPodman(socketPath string) (context.Context, error) {
114+
const (
115+
retryInterval = 5 * time.Second
116+
timeout = 5 * time.Minute
117+
)
118+
119+
deadline := time.Now().Add(timeout)
120+
121+
var ctx context.Context
122+
var err error
123+
124+
for time.Now().Before(deadline) {
125+
ctx, err = bindings.NewConnection(context.Background(), fmt.Sprintf("unix:%s", socketPath))
126+
if err == nil {
127+
log.Debugf("Connected to Podman successfully!")
128+
return ctx, nil
129+
}
130+
131+
log.Debugf("Failed to connect to Podman. Retrying in %s seconds...", retryInterval.String())
132+
time.Sleep(retryInterval)
133+
}
134+
135+
return nil, fmt.Errorf("Unable to connect to Podman after %v: %v", timeout, err)
136+
}
137+
138+
func createBootcContainer(ctx context.Context, image string, bootcCmdLine []string) (string, error) {
139+
log.Debugf("Create bootc container with cmdline: %v", bootcCmdLine)
140+
specGen := &specgen.SpecGenerator{
141+
ContainerBasicConfig: specgen.ContainerBasicConfig{
142+
Command: bootcCmdLine,
143+
Stdin: utils.Ptr(true),
144+
PidNS: specgen.Namespace{
145+
NSMode: specgen.Host,
146+
},
147+
},
148+
ContainerStorageConfig: specgen.ContainerStorageConfig{
149+
Image: image,
150+
Mounts: []ocispec.Mount{
151+
{
152+
Destination: "/var/lib/containers",
153+
Source: "/var/lib/containers",
154+
Type: "bind",
155+
},
156+
{
157+
Destination: "/var/lib/containers/storage",
158+
Source: "/usr/lib/bootc/storage",
159+
Type: "bind",
160+
},
161+
{
162+
Destination: "/dev",
163+
Source: "/dev",
164+
Type: "bind",
165+
},
166+
{
167+
Destination: "/output",
168+
Source: "/usr/lib/bootc/output",
169+
Type: "bind",
170+
},
171+
{
172+
Destination: "/config",
173+
Source: "/usr/lib/bootc/config",
174+
Type: "bind",
175+
},
176+
},
177+
},
178+
ContainerSecurityConfig: specgen.ContainerSecurityConfig{
179+
Privileged: utils.Ptr(true),
180+
SelinuxOpts: []string{"type:unconfined_t"},
181+
},
182+
ContainerCgroupConfig: specgen.ContainerCgroupConfig{},
183+
}
184+
if err := specGen.Validate(); err != nil {
185+
return "", err
186+
}
187+
response, err := containers.CreateWithSpec(ctx, specGen, &containers.CreateOptions{})
188+
if err != nil {
189+
return "", err
190+
}
191+
192+
return response.ID, nil
193+
}
194+
195+
func fetchLogsAfterExit(ctx context.Context, containerID string) error {
196+
stdoutCh := make(chan string)
197+
stderrCh := make(chan string)
198+
199+
// Start log streaming
200+
go func() {
201+
logOpts := new(containers.LogOptions).WithFollow(true).WithStdout(true).WithStderr(true)
202+
203+
err := containers.Logs(ctx, containerID, logOpts, stdoutCh, stderrCh)
204+
if err != nil {
205+
log.Errorf("Error streaming logs: %v\n", err)
206+
}
207+
close(stdoutCh)
208+
close(stderrCh)
209+
}()
210+
211+
go func() {
212+
for line := range stdoutCh {
213+
fmt.Fprintf(os.Stdout, "%s", line)
214+
}
215+
}()
216+
go func() {
217+
for line := range stderrCh {
218+
fmt.Fprintf(os.Stderr, "%s", line)
219+
}
220+
}()
221+
222+
exitCode, err := containers.Wait(ctx, containerID, new(containers.WaitOptions).
223+
WithCondition([]define.ContainerStatus{define.ContainerStateExited}))
224+
if err != nil {
225+
return fmt.Errorf("failed to wait for container: %w", err)
226+
}
227+
if exitCode != 0 {
228+
return fmt.Errorf("bootc command failed: %d", exitCode)
229+
}
230+
231+
return nil
232+
}
233+
234+
func RunPodmanCmd(socketPath string, image string, bootcCmdLine []string) error {
235+
ctx, err := connectPodman(socketPath)
236+
if err != nil {
237+
return fmt.Errorf("Failed to connect to Podman service: %v", err)
238+
}
239+
240+
name, err := createBootcContainer(ctx, image, bootcCmdLine)
241+
if err != nil {
242+
return fmt.Errorf("failed to create the bootc container: %v", err)
243+
}
244+
245+
if err := containers.Start(ctx, name, &containers.StartOptions{}); err != nil {
246+
return fmt.Errorf("failed to start the bootc container: %v", err)
247+
}
248+
249+
if err := fetchLogsAfterExit(ctx, name); err != nil {
250+
return fmt.Errorf("failed executing bootc: %v", err)
251+
}
252+
253+
return nil
254+
}
255+
256+
func DefaultPodmanSocket() string {
257+
if envSock := os.Getenv("DOCKER_HOST"); envSock != "" {
258+
return envSock
259+
}
260+
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
261+
if runtimeDir != "" {
262+
return filepath.Join(runtimeDir, "podman", "podman.sock")
263+
}
264+
usr, err := user.Current()
265+
if err == nil && usr.Uid != "0" {
266+
return "/run/user/" + usr.Uid + "/podman/podman.sock"
267+
}
268+
269+
return "/run/podman/podman.sock"
270+
}
271+
272+
func DefaultContainerStorage() string {
273+
usr, err := user.Current()
274+
if err == nil && usr.Uid != "0" {
275+
homeDir := os.Getenv("HOME")
276+
if homeDir != "" {
277+
return filepath.Join(homeDir, ".local/share/containers/storage")
278+
}
279+
}
280+
281+
return "/var/lib/containers/storage"
282+
}

0 commit comments

Comments
 (0)