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