Skip to content

Commit 6fa974d

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 6fa974d

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed

pkg/podman/podman.go

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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: %v", dir)
109+
return nil
110+
}
111+
112+
func connectPodman(socketPath string) (context.Context, error) {
113+
const (
114+
retryInterval = 5 * time.Second
115+
timeout = 5 * time.Minute
116+
)
117+
118+
deadline := time.Now().Add(timeout)
119+
120+
var ctx context.Context
121+
var err error
122+
123+
for time.Now().Before(deadline) {
124+
ctx, err = bindings.NewConnection(context.Background(), fmt.Sprintf("unix:%s", socketPath))
125+
if err == nil {
126+
log.Debugf("Connected to Podman successfully!")
127+
return ctx, nil
128+
}
129+
130+
log.Debugf("Failed to connect to Podman. Retrying in %s seconds...", retryInterval.String())
131+
time.Sleep(retryInterval)
132+
}
133+
134+
return nil, fmt.Errorf("Unable to connect to Podman after %v: %v", timeout, err)
135+
}
136+
137+
func createBootcContainer(ctx context.Context, image string, bootcCmdLine []string) (string, error) {
138+
log.Debugf("Create bootc container with cmdline: %v", bootcCmdLine)
139+
specGen := &specgen.SpecGenerator{
140+
ContainerBasicConfig: specgen.ContainerBasicConfig{
141+
Command: bootcCmdLine,
142+
Stdin: utils.Ptr(true),
143+
PidNS: specgen.Namespace{
144+
NSMode: specgen.Host,
145+
},
146+
},
147+
ContainerStorageConfig: specgen.ContainerStorageConfig{
148+
Image: image,
149+
Mounts: []ocispec.Mount{
150+
{
151+
Destination: "/var/lib/containers",
152+
Source: "/var/lib/containers",
153+
Type: "bind",
154+
},
155+
{
156+
Destination: "/var/lib/containers/storage",
157+
Source: "/usr/lib/bootc/storage",
158+
Type: "bind",
159+
},
160+
{
161+
Destination: "/dev",
162+
Source: "/dev",
163+
Type: "bind",
164+
},
165+
{
166+
Destination: "/output",
167+
Source: "/usr/lib/bootc/output",
168+
Type: "bind",
169+
},
170+
{
171+
Destination: "/config",
172+
Source: "/usr/lib/bootc/config",
173+
Type: "bind",
174+
},
175+
},
176+
},
177+
ContainerSecurityConfig: specgen.ContainerSecurityConfig{
178+
Privileged: utils.Ptr(true),
179+
SelinuxOpts: []string{"type:unconfined_t"},
180+
},
181+
ContainerCgroupConfig: specgen.ContainerCgroupConfig{},
182+
}
183+
if err := specGen.Validate(); err != nil {
184+
return "", err
185+
}
186+
response, err := containers.CreateWithSpec(ctx, specGen, &containers.CreateOptions{})
187+
if err != nil {
188+
return "", err
189+
}
190+
191+
return response.ID, nil
192+
}
193+
194+
func fetchLogsAfterExit(ctx context.Context, containerID string) error {
195+
stdoutCh := make(chan string)
196+
stderrCh := make(chan string)
197+
198+
// Start log streaming
199+
go func() {
200+
logOpts := new(containers.LogOptions).WithFollow(true).WithStdout(true).WithStderr(true)
201+
202+
err := containers.Logs(ctx, containerID, logOpts, stdoutCh, stderrCh)
203+
if err != nil {
204+
log.Errorf("Error streaming logs: %v\n", err)
205+
}
206+
close(stdoutCh)
207+
close(stderrCh)
208+
}()
209+
210+
go func() {
211+
for line := range stdoutCh {
212+
fmt.Fprintf(os.Stdout, "%s", line)
213+
}
214+
}()
215+
go func() {
216+
for line := range stderrCh {
217+
fmt.Fprintf(os.Stderr, "%s", line)
218+
}
219+
}()
220+
221+
exitCode, err := containers.Wait(ctx, containerID, new(containers.WaitOptions).
222+
WithCondition([]define.ContainerStatus{define.ContainerStateExited}))
223+
if err != nil {
224+
return fmt.Errorf("failed to wait for container: %w", err)
225+
}
226+
if exitCode != 0 {
227+
fmt.Errorf("bootc command failed: %d", exitCode)
228+
}
229+
230+
return nil
231+
}
232+
233+
func RunPodmanCmd(socketPath string, image string, bootcCmdLine []string) error {
234+
ctx, err := connectPodman(socketPath)
235+
if err != nil {
236+
return fmt.Errorf("Failed to connect to Podman service: %v", err)
237+
}
238+
239+
name, err := createBootcContainer(ctx, image, bootcCmdLine)
240+
if err != nil {
241+
return fmt.Errorf("failed to create the bootc container: %v", err)
242+
}
243+
244+
if err := containers.Start(ctx, name, &containers.StartOptions{}); err != nil {
245+
return fmt.Errorf("failed to start the bootc container: %v", err)
246+
}
247+
248+
if err := fetchLogsAfterExit(ctx, name); err != nil {
249+
return fmt.Errorf("failed executing bootc: %s %s: %v", err)
250+
}
251+
252+
return nil
253+
}
254+
255+
func DefaultPodmanSocket() string {
256+
if envSock := os.Getenv("DOCKER_HOST"); envSock != "" {
257+
return envSock
258+
}
259+
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
260+
if runtimeDir != "" {
261+
return filepath.Join(runtimeDir, "podman", "podman.sock")
262+
}
263+
usr, err := user.Current()
264+
if err == nil && usr.Uid != "0" {
265+
return "/run/user/" + usr.Uid + "/podman/podman.sock"
266+
}
267+
268+
return "/run/podman/podman.sock"
269+
}
270+
271+
func DefaultContainerStorage() string {
272+
usr, err := user.Current()
273+
if err == nil && usr.Uid != "0" {
274+
homeDir := os.Getenv("HOME")
275+
if homeDir != "" {
276+
return filepath.Join(homeDir, ".local/share/containers/storage")
277+
}
278+
}
279+
280+
return "/var/lib/containers/storage"
281+
}

0 commit comments

Comments
 (0)