Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions cmd/check-host-config/check/container_embedding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package check

import (
"encoding/json"
"fmt"
"log"
"strings"

"github.com/osbuild/images/internal/buildconfig"
)

func init() {
RegisterCheck(Metadata{
Name: "container-embedding",
RequiresBlueprint: true,
}, containerEmbeddingCheck)
}

type podmanImage struct {
Names []string `json:"Names"`
}

func containerEmbeddingCheck(meta *Metadata, config *buildconfig.BuildConfig) error {
containers := config.Blueprint.Containers
if len(containers) == 0 {
return Skip("no containers to check")
}

stdout, _, _, err := Exec("sudo", "podman", "images", "--format", "json")
if err != nil {
return Fail("failed to list podman images:", err)
}

var images []podmanImage
if err := json.Unmarshal(stdout, &images); err != nil {
return Fail("failed to parse podman images output:", err)
}

for _, ctr := range containers {
// The blueprint Name, when set, is used as the local name for the
// container in the image storage (see Spec.LocalName). When empty,
// the source reference is used instead.
needle := ctr.Source
if ctr.Name != "" {
needle = ctr.Name
}
if needle == "" {
continue
}

found := false
for _, img := range images {
for _, name := range img.Names {
if strings.HasPrefix(name, needle) {
found = true
break
}
}
if found {
break
}
}

if !found {
return Fail(fmt.Sprintf("embedded container %q (source %q) not found in podman images", needle, ctr.Source))
}
log.Printf("Container %q found in podman images\n", needle)
}

return Pass()
}
117 changes: 117 additions & 0 deletions cmd/check-host-config/check/container_embedding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package check_test

import (
"errors"
"testing"

"github.com/osbuild/blueprint/pkg/blueprint"
check "github.com/osbuild/images/cmd/check-host-config/check"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestContainerEmbeddingCheck(t *testing.T) {
tests := []struct {
name string
containers []blueprint.Container
mockExec map[string]ExecResult
wantErr error
}{
{
name: "skip when no containers",
containers: nil,
wantErr: check.ErrCheckSkipped,
},
{
name: "pass when container is found",
containers: []blueprint.Container{
{Source: "registry.example.com/test"},
},
mockExec: map[string]ExecResult{
"sudo podman images --format json": {
Stdout: []byte(`[{"Names":["registry.example.com/test:latest"]}]`),
},
},
},
{
name: "fail when container is not found",
containers: []blueprint.Container{
{Source: "registry.example.com/missing"},
},
mockExec: map[string]ExecResult{
"sudo podman images --format json": {
Stdout: []byte(`[{"Names":["registry.example.com/other:latest"]}]`),
},
},
wantErr: check.ErrCheckFailed,
},
{
name: "fail when podman command fails",
containers: []blueprint.Container{
{Source: "registry.example.com/test"},
},
mockExec: map[string]ExecResult{
"sudo podman images --format json": {
Err: errors.New("podman not found"),
},
},
wantErr: check.ErrCheckFailed,
},
{
name: "pass with multiple containers",
containers: []blueprint.Container{
{Source: "registry.example.com/first"},
{Source: "registry.example.com/second"},
},
mockExec: map[string]ExecResult{
"sudo podman images --format json": {
Stdout: []byte(`[{"Names":["registry.example.com/first:latest"]},{"Names":["registry.example.com/second:v1"]}]`),
},
},
},
{
name: "pass when custom name matches",
containers: []blueprint.Container{
{Source: "registry.example.com/source-image", Name: "custom-name:v1"},
},
mockExec: map[string]ExecResult{
"sudo podman images --format json": {
Stdout: []byte(`[{"Names":["custom-name:v1"]}]`),
},
},
},
{
name: "fail when custom name does not match",
containers: []blueprint.Container{
{Source: "registry.example.com/source-image", Name: "custom-name:v1"},
},
mockExec: map[string]ExecResult{
"sudo podman images --format json": {
Stdout: []byte(`[{"Names":["registry.example.com/source-image:latest"]}]`),
},
},
wantErr: check.ErrCheckFailed,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
installMockExec(t, tt.mockExec)

chk, found := check.FindCheckByName("container-embedding")
require.True(t, found, "container-embedding check not found")

config := buildConfigWithBlueprint(func(bp *blueprint.Blueprint) {
bp.Containers = tt.containers
})

err := chk.Func(chk.Meta, config)
if tt.wantErr != nil {
require.Error(t, err)
assert.True(t, errors.Is(err, tt.wantErr))
} else {
require.NoError(t, err)
}
})
}
}
74 changes: 74 additions & 0 deletions cmd/check-host-config/check/podman_network_backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package check

import (
"encoding/json"
"log"

"github.com/osbuild/images/internal/buildconfig"
)

func init() {
RegisterCheck(Metadata{
Name: "podman-network-backend",
RequiresBlueprint: true,
}, podmanNetworkBackendCheck)
}

type podmanInfo struct {
Host struct {
NetworkBackend string `json:"networkBackend"`
} `json:"host"`
}

func getPodmanNetworkBackend(sudo bool) (string, error) {
var stdout []byte
var err error

if sudo {
stdout, _, _, err = Exec("sudo", "podman", "info", "--format", "json")
} else {
stdout, _, _, err = Exec("podman", "info", "--format", "json")
}
if err != nil {
return "", err
}

var info podmanInfo
if err := json.Unmarshal(stdout, &info); err != nil {
return "", err
}

backend := info.Host.NetworkBackend
if backend == "" {
backend = "undefined"
}
return backend, nil
}

// podmanNetworkBackendCheck verifies that rootful and rootless podman use the
// same network backend. When containers are embedded into the image as root,
// certain podman versions may interpret the existing storage as a migration
// and fall back to 'cni' for rootful only, creating an inconsistency.
func podmanNetworkBackendCheck(meta *Metadata, config *buildconfig.BuildConfig) error {
if len(config.Blueprint.Containers) == 0 {
return Skip("no embedded containers")
}

rootful, err := getPodmanNetworkBackend(true)
if err != nil {
return Fail("failed to get rootful podman network backend:", err)
}
log.Printf("Rootful podman network backend: %s\n", rootful)

rootless, err := getPodmanNetworkBackend(false)
if err != nil {
return Fail("failed to get rootless podman network backend:", err)
}
log.Printf("Rootless podman network backend: %s\n", rootless)

if rootful != rootless {
return Fail("podman network backends are inconsistent: rootful="+rootful, "rootless="+rootless)
}

return Pass()
}
88 changes: 88 additions & 0 deletions cmd/check-host-config/check/podman_network_backend_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package check_test

import (
"errors"
"testing"

"github.com/osbuild/blueprint/pkg/blueprint"
check "github.com/osbuild/images/cmd/check-host-config/check"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestPodmanNetworkBackendCheck(t *testing.T) {
tests := []struct {
name string
containers []blueprint.Container
mockExec map[string]ExecResult
wantErr error
}{
{
name: "skip when no containers",
containers: nil,
wantErr: check.ErrCheckSkipped,
},
{
name: "pass when backends match",
containers: []blueprint.Container{
{Source: "registry.example.com/test"},
},
mockExec: map[string]ExecResult{
"sudo podman info --format json": {
Stdout: []byte(`{"host":{"networkBackend":"netavark"}}`),
},
"podman info --format json": {
Stdout: []byte(`{"host":{"networkBackend":"netavark"}}`),
},
},
},
{
name: "fail when backends differ",
containers: []blueprint.Container{
{Source: "registry.example.com/test"},
},
mockExec: map[string]ExecResult{
"sudo podman info --format json": {
Stdout: []byte(`{"host":{"networkBackend":"cni"}}`),
},
"podman info --format json": {
Stdout: []byte(`{"host":{"networkBackend":"netavark"}}`),
},
},
wantErr: check.ErrCheckFailed,
},
{
name: "fail when rootful podman command fails",
containers: []blueprint.Container{
{Source: "registry.example.com/test"},
},
mockExec: map[string]ExecResult{
"sudo podman info --format json": {
Err: errors.New("podman not found"),
},
},
wantErr: check.ErrCheckFailed,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
installMockExec(t, tt.mockExec)

chk, found := check.FindCheckByName("podman-network-backend")
require.True(t, found, "podman-network-backend check not found")

config := buildConfigWithBlueprint(func(bp *blueprint.Blueprint) {
bp.Containers = tt.containers
})

err := chk.Func(chk.Meta, config)
if tt.wantErr != nil {
require.Error(t, err)
assert.True(t, errors.Is(err, tt.wantErr))
} else {
require.NoError(t, err)
}
})
}
}
1 change: 1 addition & 0 deletions data/distrodefs/rhel-9/imagetypes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,7 @@
image_config:
default:
default_kernel: "kernel"
podman_default_net_backend: "netavark"
default_oscap_datastream: "/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml"
install_weak_deps: true
locale: "C.UTF-8"
Expand Down
26 changes: 26 additions & 0 deletions pkg/container/podman.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package container

import "github.com/osbuild/images/pkg/customizations/fsnode"

// NetworkBackend is the type of network backend used by Podman.
type NetworkBackend string

const (
NetworkBackendCNI NetworkBackend = "cni"
NetworkBackendNetavark NetworkBackend = "netavark"
)

// GenDefaultNetworkBackendFile creates an fsnode.File that writes the given
// network backend name to /var/lib/containers/storage/defaultNetworkBackend.
//
// Certain versions of Podman fall back to 'cni' when they find existing
// container images in the system storage, assuming a migration from an older
// version. Writing this file prevents that behavior and forces Podman to use
// the specified backend.
func GenDefaultNetworkBackendFile(backend NetworkBackend) (*fsnode.File, error) {
file, err := fsnode.NewFile("/var/lib/containers/storage/defaultNetworkBackend", nil, nil, nil, []byte(backend))
if err != nil {
return nil, err
}
return file, nil
}
Loading
Loading