mirror of
https://github.com/containers/podman.git
synced 2025-05-17 23:26:08 +08:00

This will make it easier to structure the API, at the cost of making it a bit more opaque about which parts of PodmanExecOptions are implemented where. Should not change behavior. Signed-off-by: Miloslav Trmač <mitr@redhat.com>
1592 lines
48 KiB
Go
1592 lines
48 KiB
Go
//go:build linux || freebsd
|
||
|
||
package integration
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"io/fs"
|
||
"math/rand"
|
||
"net"
|
||
"net/url"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"slices"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/containers/common/pkg/cgroups"
|
||
"github.com/containers/podman/v5/libpod/define"
|
||
"github.com/containers/podman/v5/pkg/inspect"
|
||
. "github.com/containers/podman/v5/test/utils"
|
||
"github.com/containers/podman/v5/utils"
|
||
"github.com/containers/storage/pkg/lockfile"
|
||
"github.com/containers/storage/pkg/reexec"
|
||
"github.com/containers/storage/pkg/stringid"
|
||
jsoniter "github.com/json-iterator/go"
|
||
. "github.com/onsi/ginkgo/v2"
|
||
. "github.com/onsi/gomega"
|
||
. "github.com/onsi/gomega/gexec"
|
||
"github.com/sirupsen/logrus"
|
||
)
|
||
|
||
var (
|
||
//lint:ignore ST1003
|
||
PODMAN_BINARY string //nolint:revive,stylecheck
|
||
INTEGRATION_ROOT string //nolint:revive,stylecheck
|
||
CGROUP_MANAGER = "systemd" //nolint:revive,stylecheck
|
||
RESTORE_IMAGES = []string{ALPINE, BB, NGINX_IMAGE} //nolint:revive,stylecheck
|
||
defaultWaitTimeout = 90
|
||
CGROUPSV2, _ = cgroups.IsCgroup2UnifiedMode()
|
||
)
|
||
|
||
// PodmanTestIntegration struct for command line options
|
||
type PodmanTestIntegration struct {
|
||
PodmanTest
|
||
ConmonBinary string
|
||
QuadletBinary string
|
||
Root string
|
||
NetworkConfigDir string
|
||
OCIRuntime string
|
||
RunRoot string
|
||
StorageOptions string
|
||
SignaturePolicyPath string
|
||
CgroupManager string
|
||
Host HostOS
|
||
TmpDir string
|
||
}
|
||
|
||
var GlobalTmpDir string // Single top-level tmpdir for all tests
|
||
var LockTmpDir string
|
||
|
||
// PodmanSessionIntegration struct for command line session
|
||
type PodmanSessionIntegration struct {
|
||
*PodmanSession
|
||
}
|
||
|
||
type testResult struct {
|
||
name string
|
||
length float64
|
||
}
|
||
|
||
type testResultsSorted []testResult
|
||
|
||
func (a testResultsSorted) Len() int { return len(a) }
|
||
func (a testResultsSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||
|
||
type testResultsSortedLength struct{ testResultsSorted }
|
||
|
||
func (a testResultsSorted) Less(i, j int) bool { return a[i].length < a[j].length }
|
||
|
||
func TestMain(m *testing.M) {
|
||
if reexec.Init() {
|
||
return
|
||
}
|
||
os.Exit(m.Run())
|
||
}
|
||
|
||
// TestLibpod ginkgo master function
|
||
func TestLibpod(t *testing.T) {
|
||
if os.Getenv("NOCACHE") == "1" {
|
||
CACHE_IMAGES = []string{}
|
||
RESTORE_IMAGES = []string{}
|
||
}
|
||
RegisterFailHandler(Fail)
|
||
RunSpecs(t, "Libpod Suite")
|
||
}
|
||
|
||
var (
|
||
tempdir string // Working dir for _one_ subtest
|
||
err error
|
||
podmanTest *PodmanTestIntegration
|
||
safeIPOctets [2]uint8
|
||
timingsFile *os.File
|
||
|
||
_ = BeforeEach(func() {
|
||
tempdir, err = os.MkdirTemp(GlobalTmpDir, "subtest-")
|
||
Expect(err).ToNot(HaveOccurred())
|
||
podmanTest = PodmanTestCreate(tempdir)
|
||
podmanTest.Setup()
|
||
// see GetSafeIPAddress() below
|
||
safeIPOctets[0] = uint8(GinkgoT().ParallelProcess()) + 128
|
||
safeIPOctets[1] = 2
|
||
})
|
||
|
||
_ = AfterEach(func() {
|
||
// First unset CONTAINERS_CONF before doing Cleanup() to prevent
|
||
// invalid containers.conf files to fail the cleanup.
|
||
os.Unsetenv("CONTAINERS_CONF")
|
||
os.Unsetenv("CONTAINERS_CONF_OVERRIDE")
|
||
os.Unsetenv("PODMAN_CONNECTIONS_CONF")
|
||
podmanTest.Cleanup()
|
||
f := CurrentSpecReport()
|
||
processTestResult(f)
|
||
})
|
||
)
|
||
|
||
const (
|
||
// lockdir - do not use directly; use LockTmpDir
|
||
lockdir = "libpodlock"
|
||
// imageCacheDir - do not use directly use ImageCacheDir
|
||
imageCacheDir = "imagecachedir"
|
||
)
|
||
|
||
var netnsFiles []fs.DirEntry
|
||
|
||
func getNetnsDir() string {
|
||
if isRootless() {
|
||
var path string
|
||
if env, ok := os.LookupEnv("XDG_RUNTIME_DIR"); ok {
|
||
path = env
|
||
} else {
|
||
path = fmt.Sprintf("/run/user/%d", os.Getuid())
|
||
}
|
||
return filepath.Join(path, "netns")
|
||
}
|
||
// root is hard coded to
|
||
return "/run/netns"
|
||
}
|
||
|
||
var _ = SynchronizedBeforeSuite(func() []byte {
|
||
globalTmpDir, err := os.MkdirTemp("", "podman-e2e-")
|
||
Expect(err).ToNot(HaveOccurred())
|
||
|
||
netnsFiles, err = os.ReadDir(getNetnsDir())
|
||
// dir might not exists which is fine
|
||
if !errors.Is(err, fs.ErrNotExist) {
|
||
Expect(err).ToNot(HaveOccurred())
|
||
}
|
||
|
||
// make cache dir
|
||
ImageCacheDir = filepath.Join(globalTmpDir, imageCacheDir)
|
||
err = os.MkdirAll(ImageCacheDir, 0700)
|
||
Expect(err).ToNot(HaveOccurred())
|
||
|
||
// Cache images
|
||
cwd, _ := os.Getwd()
|
||
INTEGRATION_ROOT = filepath.Join(cwd, "../../")
|
||
podman := PodmanTestSetup(filepath.Join(globalTmpDir, "image-init"))
|
||
|
||
// Pull cirros but don't put it into the cache
|
||
pullImages := []string{CIRROS_IMAGE, volumeTest}
|
||
pullImages = append(pullImages, CACHE_IMAGES...)
|
||
for _, image := range pullImages {
|
||
podman.createArtifact(image)
|
||
}
|
||
|
||
if err := os.MkdirAll(filepath.Join(ImageCacheDir, podman.ImageCacheFS+"-images"), 0777); err != nil {
|
||
GinkgoWriter.Printf("%q\n", err)
|
||
os.Exit(1)
|
||
}
|
||
podman.Root = ImageCacheDir
|
||
// If running localized tests, the cache dir is created and populated. if the
|
||
// tests are remote, this is a no-op
|
||
populateCache(podman)
|
||
|
||
if err := os.MkdirAll(filepath.Join(globalTmpDir, lockdir), 0700); err != nil {
|
||
GinkgoWriter.Printf("%q\n", err)
|
||
os.Exit(1)
|
||
}
|
||
|
||
// If running remote, we need to stop the associated podman system service
|
||
if podman.RemoteTest {
|
||
podman.StopRemoteService()
|
||
}
|
||
|
||
// remove temporary podman files; images are now cached in ImageCacheDir
|
||
rmAll(podman.PodmanBinary, podman.TempDir)
|
||
|
||
return []byte(globalTmpDir)
|
||
}, func(data []byte) {
|
||
cwd, _ := os.Getwd()
|
||
INTEGRATION_ROOT = filepath.Join(cwd, "../../")
|
||
GlobalTmpDir = string(data)
|
||
ImageCacheDir = filepath.Join(GlobalTmpDir, imageCacheDir)
|
||
LockTmpDir = filepath.Join(GlobalTmpDir, lockdir)
|
||
|
||
timingsFile, err = os.Create(fmt.Sprintf("%s/timings-%d", LockTmpDir, GinkgoParallelProcess()))
|
||
Expect(err).ToNot(HaveOccurred())
|
||
})
|
||
|
||
func (p *PodmanTestIntegration) Setup() {
|
||
cwd, _ := os.Getwd()
|
||
INTEGRATION_ROOT = filepath.Join(cwd, "../../")
|
||
}
|
||
|
||
var _ = SynchronizedAfterSuite(func() {
|
||
timingsFile.Close()
|
||
timingsFile = nil
|
||
},
|
||
func() {
|
||
// perform a netns leak check after all tests run
|
||
newNetnsFiles, err := os.ReadDir(getNetnsDir())
|
||
if !errors.Is(err, fs.ErrNotExist) {
|
||
Expect(err).ToNot(HaveOccurred())
|
||
}
|
||
Expect(newNetnsFiles).To(ConsistOf(netnsFiles), "Netns files were leaked")
|
||
|
||
testTimings := make(testResultsSorted, 0, 2000)
|
||
for i := 1; i <= GinkgoT().ParallelTotal(); i++ {
|
||
f, err := os.Open(fmt.Sprintf("%s/timings-%d", LockTmpDir, i))
|
||
Expect(err).ToNot(HaveOccurred())
|
||
defer f.Close()
|
||
scanner := bufio.NewScanner(f)
|
||
for scanner.Scan() {
|
||
text := scanner.Text()
|
||
name, durationString, ok := strings.Cut(text, "\t\t")
|
||
if !ok {
|
||
Fail(fmt.Sprintf("incorrect timing line: %q", text))
|
||
}
|
||
duration, err := strconv.ParseFloat(durationString, 64)
|
||
Expect(err).ToNot(HaveOccurred(), "failed to parse float from timings file")
|
||
testTimings = append(testTimings, testResult{name: name, length: duration})
|
||
}
|
||
if err := scanner.Err(); err != nil {
|
||
Expect(err).ToNot(HaveOccurred(), "read timings %d", i)
|
||
}
|
||
}
|
||
sort.Sort(testResultsSortedLength{testTimings})
|
||
GinkgoWriter.Println("integration timing results")
|
||
for _, result := range testTimings {
|
||
GinkgoWriter.Printf("%s\t\t%f\n", result.name, result.length)
|
||
}
|
||
|
||
cwd, _ := os.Getwd()
|
||
rmAll(getPodmanBinary(cwd), GlobalTmpDir)
|
||
})
|
||
|
||
func getPodmanBinary(cwd string) string {
|
||
podmanBinary := filepath.Join(cwd, "../../bin/podman")
|
||
if os.Getenv("PODMAN_BINARY") != "" {
|
||
podmanBinary = os.Getenv("PODMAN_BINARY")
|
||
}
|
||
return podmanBinary
|
||
}
|
||
|
||
// PodmanTestCreate creates a PodmanTestIntegration instance for the tests
|
||
func PodmanTestCreateUtil(tempDir string, remote bool) *PodmanTestIntegration {
|
||
host := GetHostDistributionInfo()
|
||
cwd, _ := os.Getwd()
|
||
|
||
root := filepath.Join(tempDir, "root")
|
||
podmanBinary := getPodmanBinary(cwd)
|
||
|
||
podmanRemoteBinary := os.Getenv("PODMAN_REMOTE_BINARY")
|
||
if podmanRemoteBinary == "" {
|
||
podmanRemoteBinary = filepath.Join(cwd, "../../bin/podman-remote")
|
||
}
|
||
|
||
quadletBinary := os.Getenv("QUADLET_BINARY")
|
||
if quadletBinary == "" {
|
||
quadletBinary = filepath.Join(cwd, "../../bin/quadlet")
|
||
}
|
||
|
||
conmonBinary := os.Getenv("CONMON_BINARY")
|
||
if conmonBinary == "" {
|
||
conmonBinary = "/usr/libexec/podman/conmon"
|
||
if _, err := os.Stat(conmonBinary); errors.Is(err, os.ErrNotExist) {
|
||
conmonBinary = "/usr/bin/conmon"
|
||
}
|
||
}
|
||
|
||
cgroupManager := os.Getenv("CGROUP_MANAGER")
|
||
if cgroupManager == "" {
|
||
cgroupManager = CGROUP_MANAGER
|
||
}
|
||
|
||
ociRuntime := os.Getenv("OCI_RUNTIME")
|
||
if ociRuntime == "" {
|
||
ociRuntime = "crun"
|
||
}
|
||
os.Setenv("DISABLE_HC_SYSTEMD", "true")
|
||
|
||
dbBackend := "sqlite"
|
||
if os.Getenv("PODMAN_DB") == "boltdb" {
|
||
dbBackend = "boltdb"
|
||
}
|
||
|
||
networkBackend := Netavark
|
||
networkConfigDir := "/etc/containers/networks"
|
||
if isRootless() {
|
||
networkConfigDir = filepath.Join(root, "etc", "networks")
|
||
}
|
||
|
||
if strings.ToLower(os.Getenv("NETWORK_BACKEND")) == "cni" {
|
||
networkBackend = CNI
|
||
networkConfigDir = "/etc/cni/net.d"
|
||
if isRootless() {
|
||
networkConfigDir = filepath.Join(os.Getenv("HOME"), ".config/cni/net.d")
|
||
}
|
||
}
|
||
|
||
if err := os.MkdirAll(root, 0755); err != nil {
|
||
panic(err)
|
||
}
|
||
|
||
if err := os.MkdirAll(networkConfigDir, 0755); err != nil {
|
||
panic(err)
|
||
}
|
||
|
||
storageFs := STORAGE_FS
|
||
if isRootless() {
|
||
storageFs = ROOTLESS_STORAGE_FS
|
||
}
|
||
|
||
storageOptions := STORAGE_OPTIONS
|
||
if os.Getenv("STORAGE_FS") != "" {
|
||
storageFs = os.Getenv("STORAGE_FS")
|
||
storageOptions = "--storage-driver " + storageFs
|
||
|
||
// Look for STORAGE_OPTIONS_OVERLAY / STORAGE_OPTIONS_VFS
|
||
extraOptions := os.Getenv("STORAGE_OPTIONS_" + strings.ToUpper(storageFs))
|
||
if extraOptions != "" {
|
||
storageOptions += " --storage-opt " + extraOptions
|
||
}
|
||
}
|
||
|
||
p := &PodmanTestIntegration{
|
||
PodmanTest: PodmanTest{
|
||
PodmanBinary: podmanBinary,
|
||
RemotePodmanBinary: podmanRemoteBinary,
|
||
TempDir: tempDir,
|
||
RemoteTest: remote,
|
||
ImageCacheFS: storageFs,
|
||
ImageCacheDir: ImageCacheDir,
|
||
NetworkBackend: networkBackend,
|
||
DatabaseBackend: dbBackend,
|
||
},
|
||
ConmonBinary: conmonBinary,
|
||
QuadletBinary: quadletBinary,
|
||
Root: root,
|
||
TmpDir: tempDir,
|
||
NetworkConfigDir: networkConfigDir,
|
||
OCIRuntime: ociRuntime,
|
||
RunRoot: filepath.Join(tempDir, "runroot"),
|
||
StorageOptions: storageOptions,
|
||
SignaturePolicyPath: filepath.Join(INTEGRATION_ROOT, "test/policy.json"),
|
||
CgroupManager: cgroupManager,
|
||
Host: host,
|
||
}
|
||
|
||
if remote {
|
||
var pathPrefix string
|
||
if !isRootless() {
|
||
pathPrefix = "/run/podman/podman"
|
||
} else {
|
||
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
||
pathPrefix = filepath.Join(runtimeDir, "podman")
|
||
}
|
||
// We want to avoid collisions in socket paths, but using the
|
||
// socket directly for a collision check doesn’t work; bind(2) on AF_UNIX
|
||
// creates the file, and we need to pass a unique path now before the bind(2)
|
||
// happens. So, use a podman-%s.sock-lock empty file as a marker.
|
||
tries := 0
|
||
for {
|
||
uuid := stringid.GenerateRandomID()
|
||
lockPath := fmt.Sprintf("%s-%s.sock-lock", pathPrefix, uuid)
|
||
lockFile, err := os.OpenFile(lockPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0700)
|
||
if err == nil {
|
||
lockFile.Close()
|
||
p.RemoteSocketLock = lockPath
|
||
p.RemoteSocket = fmt.Sprintf("unix://%s-%s.sock", pathPrefix, uuid)
|
||
break
|
||
}
|
||
tries++
|
||
if tries >= 1000 {
|
||
panic("Too many RemoteSocket collisions")
|
||
}
|
||
}
|
||
}
|
||
|
||
// Set up registries.conf ENV variable
|
||
p.setDefaultRegistriesConfigEnv()
|
||
// Rewrite the PodmanAsUser function
|
||
p.PodmanMakeOptions = p.makeOptions
|
||
return p
|
||
}
|
||
|
||
func (p *PodmanTestIntegration) AddImageToRWStore(image string) {
|
||
if err := p.RestoreArtifact(image); err != nil {
|
||
logrus.Errorf("Unable to restore %s to RW store", image)
|
||
}
|
||
}
|
||
|
||
func imageTarPath(image string) string {
|
||
cacheDir := os.Getenv("PODMAN_TEST_IMAGE_CACHE_DIR")
|
||
if cacheDir == "" {
|
||
// Avoid /tmp: it may be tmpfs, and these images are large
|
||
cacheDir = "/var/tmp"
|
||
}
|
||
|
||
// e.g., registry.com/fubar:latest -> registry.com-fubar-latest.tar
|
||
imageCacheName := strings.ReplaceAll(strings.ReplaceAll(image, ":", "-"), "/", "-") + ".tar"
|
||
|
||
return filepath.Join(cacheDir, imageCacheName)
|
||
}
|
||
|
||
func (p *PodmanTestIntegration) pullImage(image string, toCache bool) {
|
||
if toCache {
|
||
oldRoot := p.Root
|
||
p.Root = p.ImageCacheDir
|
||
defer func() {
|
||
p.Root = oldRoot
|
||
}()
|
||
}
|
||
for try := 0; try < 3; try++ {
|
||
podmanSession := p.PodmanBase([]string{"pull", image}, toCache, true)
|
||
pull := PodmanSessionIntegration{podmanSession}
|
||
pull.Wait(440)
|
||
if pull.ExitCode() == 0 {
|
||
break
|
||
}
|
||
if try == 2 {
|
||
Expect(pull).Should(Exit(0), "Failed after many retries")
|
||
}
|
||
|
||
GinkgoWriter.Println("Will wait and retry")
|
||
time.Sleep(time.Duration(try+1) * 5 * time.Second)
|
||
}
|
||
}
|
||
|
||
// createArtifact creates a cached image tarball in a local directory
|
||
func (p *PodmanTestIntegration) createArtifact(image string) {
|
||
if os.Getenv("NO_TEST_CACHE") != "" {
|
||
return
|
||
}
|
||
destName := imageTarPath(image)
|
||
if _, err := os.Stat(destName); os.IsNotExist(err) {
|
||
GinkgoWriter.Printf("Caching %s at %s...\n", image, destName)
|
||
|
||
p.pullImage(image, false)
|
||
|
||
save := p.PodmanNoCache([]string{"save", "-o", destName, image})
|
||
save.Wait(90)
|
||
Expect(save).Should(Exit(0))
|
||
GinkgoWriter.Printf("\n")
|
||
} else {
|
||
GinkgoWriter.Printf("[image already cached: %s]\n", destName)
|
||
}
|
||
}
|
||
|
||
// InspectImageJSON takes the session output of an inspect
|
||
// image and returns json
|
||
func (s *PodmanSessionIntegration) InspectImageJSON() []inspect.ImageData {
|
||
var i []inspect.ImageData
|
||
err := jsoniter.Unmarshal(s.Out.Contents(), &i)
|
||
Expect(err).ToNot(HaveOccurred())
|
||
return i
|
||
}
|
||
|
||
// PodmanExitCleanly runs a podman command with args, and expects it to ExitCleanly within the default timeout.
|
||
// It returns the session (to allow consuming output if desired).
|
||
func (p *PodmanTestIntegration) PodmanExitCleanly(args ...string) *PodmanSessionIntegration {
|
||
GinkgoHelper()
|
||
session := p.Podman(args)
|
||
session.WaitWithDefaultTimeout()
|
||
Expect(session).Should(ExitCleanly())
|
||
return session
|
||
}
|
||
|
||
// InspectContainer returns a container's inspect data in JSON format
|
||
func (p *PodmanTestIntegration) InspectContainer(name string) []define.InspectContainerData {
|
||
cmd := []string{"inspect", name}
|
||
session := p.Podman(cmd)
|
||
session.WaitWithDefaultTimeout()
|
||
Expect(session).Should(Exit(0))
|
||
return session.InspectContainerToJSON()
|
||
}
|
||
|
||
// Pull a single field from a container using `podman inspect --format {{ field }}`,
|
||
// and verify it against the given expected value.
|
||
func (p *PodmanTestIntegration) CheckContainerSingleField(name, field, expected string) {
|
||
inspect := p.Podman([]string{"inspect", "--format", fmt.Sprintf("{{ %s }}", field), name})
|
||
inspect.WaitWithDefaultTimeout()
|
||
ExpectWithOffset(1, inspect).Should(Exit(0))
|
||
ExpectWithOffset(1, inspect.OutputToString()).To(Equal(expected))
|
||
}
|
||
|
||
// Check that the contents of a single file in the given container matches the expected value.
|
||
func (p *PodmanTestIntegration) CheckFileInContainer(name, filepath, expected string) {
|
||
exec := p.Podman([]string{"exec", name, "cat", filepath})
|
||
exec.WaitWithDefaultTimeout()
|
||
ExpectWithOffset(1, exec).Should(Exit(0))
|
||
ExpectWithOffset(1, exec.OutputToString()).To(Equal(expected))
|
||
}
|
||
|
||
// Check that the contents of a single file in the given container containers the given value.
|
||
func (p *PodmanTestIntegration) CheckFileInContainerSubstring(name, filepath, expected string) {
|
||
exec := p.Podman([]string{"exec", name, "cat", filepath})
|
||
exec.WaitWithDefaultTimeout()
|
||
ExpectWithOffset(1, exec).Should(Exit(0))
|
||
ExpectWithOffset(1, exec.OutputToString()).To(ContainSubstring(expected))
|
||
}
|
||
|
||
// StopContainer stops a container with no timeout, ensuring a fast test.
|
||
func (p *PodmanTestIntegration) StopContainer(nameOrID string) {
|
||
p.PodmanExitCleanly("stop", "-t0", nameOrID)
|
||
}
|
||
|
||
func (p *PodmanTestIntegration) StopPod(nameOrID string) {
|
||
p.PodmanExitCleanly("pod", "stop", "-t0", nameOrID)
|
||
}
|
||
|
||
func processTestResult(r SpecReport) {
|
||
tr := testResult{length: r.RunTime.Seconds(), name: r.FullText()}
|
||
_, err := timingsFile.WriteString(fmt.Sprintf("%s\t\t%f\n", tr.name, tr.length))
|
||
Expect(err).ToNot(HaveOccurred(), "write timings")
|
||
}
|
||
|
||
func GetPortLock(port string) *lockfile.LockFile {
|
||
lockFile := filepath.Join(LockTmpDir, port)
|
||
lock, err := lockfile.GetLockFile(lockFile)
|
||
if err != nil {
|
||
GinkgoWriter.Println(err)
|
||
os.Exit(1)
|
||
}
|
||
lock.Lock()
|
||
return lock
|
||
}
|
||
|
||
// GetSafeIPAddress returns a sequentially allocated IP address that _should_
|
||
// be safe and unique across parallel tasks
|
||
//
|
||
// Used by tests which want to use "--ip SOMETHING-SAFE". Picking at random
|
||
// just doesn't work: we get occasional collisions. Our current approach
|
||
// allocates a /24 subnet for each ginkgo process, starting at .128.x, see
|
||
// BeforeEach() above. Unfortunately, CNI remembers each address assigned
|
||
// and assigns <previous+1> by default -- so other parallel jobs may
|
||
// get IPs in our block. The +10 leaves a gap for that. (Netavark works
|
||
// differently, allocating sequentially from .0.0, hence our .128.x).
|
||
// This heuristic will fail if run in parallel on >127 processors or if
|
||
// one test calls us more than 25 times or if some other test runs more
|
||
// than ten networked containers at the same time as any test that
|
||
// relies on GetSafeIPAddress(). I'm finding it hard to care.
|
||
//
|
||
// DO NOT USE THIS FUNCTION unless there is no possible alternative. In
|
||
// most cases you should use 'podman network create' + 'podman run --network'.
|
||
func GetSafeIPAddress() string {
|
||
safeIPOctets[1] += 10
|
||
return fmt.Sprintf("10.88.%d.%d", safeIPOctets[0], safeIPOctets[1])
|
||
}
|
||
|
||
// RunTopContainer runs a simple container in the background that
|
||
// runs top. If the name passed != "", it will have a name
|
||
func (p *PodmanTestIntegration) RunTopContainer(name string) *PodmanSessionIntegration {
|
||
return p.RunTopContainerWithArgs(name, nil)
|
||
}
|
||
|
||
// RunTopContainerWithArgs runs a simple container in the background that
|
||
// runs top. If the name passed != "", it will have a name, command args can also be passed in
|
||
func (p *PodmanTestIntegration) RunTopContainerWithArgs(name string, args []string) *PodmanSessionIntegration {
|
||
// In proxy environment, some tests need to the --http-proxy=false option (#16684)
|
||
var podmanArgs = []string{"run", "--http-proxy=false"}
|
||
if name != "" {
|
||
podmanArgs = append(podmanArgs, "--name", name)
|
||
}
|
||
podmanArgs = append(podmanArgs, args...)
|
||
podmanArgs = append(podmanArgs, "-d", ALPINE, "top", "-b")
|
||
session := p.PodmanExitCleanly(podmanArgs...)
|
||
cid := session.OutputToString()
|
||
// Output indicates that top is running, which means it's safe
|
||
// for our caller to invoke `podman stop`
|
||
if !WaitContainerReady(p, cid, "Mem:", 20, 1) {
|
||
Fail("Could not start a top container")
|
||
}
|
||
return session
|
||
}
|
||
|
||
// RunLsContainer runs a simple container in the background that
|
||
// simply runs ls. If the name passed != "", it will have a name
|
||
func (p *PodmanTestIntegration) RunLsContainer(name string) (*PodmanSessionIntegration, int, string) {
|
||
var podmanArgs = []string{"run"}
|
||
if name != "" {
|
||
podmanArgs = append(podmanArgs, "--name", name)
|
||
}
|
||
podmanArgs = append(podmanArgs, "-d", ALPINE, "ls")
|
||
session := p.Podman(podmanArgs)
|
||
session.WaitWithDefaultTimeout()
|
||
if session.ExitCode() != 0 {
|
||
return session, session.ExitCode(), session.OutputToString()
|
||
}
|
||
cid := session.OutputToString()
|
||
|
||
wsession := p.Podman([]string{"wait", cid})
|
||
wsession.WaitWithDefaultTimeout()
|
||
return session, wsession.ExitCode(), cid
|
||
}
|
||
|
||
// RunNginxWithHealthCheck runs the alpine nginx container with an optional name and adds a healthcheck into it
|
||
func (p *PodmanTestIntegration) RunNginxWithHealthCheck(name string) (*PodmanSessionIntegration, string) {
|
||
var podmanArgs = []string{"run"}
|
||
if name != "" {
|
||
podmanArgs = append(podmanArgs, "--name", name)
|
||
}
|
||
// curl without -f exits 0 even if http code >= 400!
|
||
podmanArgs = append(podmanArgs, "-dt", "-P", "--health-cmd", "curl -f http://localhost/", NGINX_IMAGE)
|
||
session := p.Podman(podmanArgs)
|
||
session.WaitWithDefaultTimeout()
|
||
return session, session.OutputToString()
|
||
}
|
||
|
||
// RunContainerWithNetworkTest runs the fedoraMinimal curl with the specified network mode.
|
||
func (p *PodmanTestIntegration) RunContainerWithNetworkTest(mode string) *PodmanSessionIntegration {
|
||
var podmanArgs = []string{"run"}
|
||
if mode != "" {
|
||
podmanArgs = append(podmanArgs, "--network", mode)
|
||
}
|
||
podmanArgs = append(podmanArgs, fedoraMinimal, "curl", "-s", "-S", "-k", "-o", "/dev/null", "http://www.redhat.com:80")
|
||
session := p.Podman(podmanArgs)
|
||
return session
|
||
}
|
||
|
||
func (p *PodmanTestIntegration) RunLsContainerInPod(name, pod string) (*PodmanSessionIntegration, int, string) {
|
||
var podmanArgs = []string{"run", "--pod", pod}
|
||
if name != "" {
|
||
podmanArgs = append(podmanArgs, "--name", name)
|
||
}
|
||
podmanArgs = append(podmanArgs, "-d", ALPINE, "ls")
|
||
session := p.Podman(podmanArgs)
|
||
session.WaitWithDefaultTimeout()
|
||
if session.ExitCode() != 0 {
|
||
return session, session.ExitCode(), session.OutputToString()
|
||
}
|
||
cid := session.OutputToString()
|
||
|
||
wsession := p.Podman([]string{"wait", cid})
|
||
wsession.WaitWithDefaultTimeout()
|
||
return session, wsession.ExitCode(), cid
|
||
}
|
||
|
||
// BuildImage uses podman build and buildah to build an image
|
||
// called imageName based on a string dockerfile
|
||
func (p *PodmanTestIntegration) BuildImage(dockerfile, imageName string, layers string, extraOptions ...string) string {
|
||
return p.buildImage(dockerfile, imageName, layers, "", extraOptions)
|
||
}
|
||
|
||
// BuildImageWithLabel uses podman build and buildah to build an image
|
||
// called imageName based on a string dockerfile, adds desired label to paramset
|
||
func (p *PodmanTestIntegration) BuildImageWithLabel(dockerfile, imageName string, layers string, label string, extraOptions ...string) string {
|
||
return p.buildImage(dockerfile, imageName, layers, label, extraOptions)
|
||
}
|
||
|
||
// PodmanPID execs podman and returns its PID
|
||
func (p *PodmanTestIntegration) PodmanPID(args []string) (*PodmanSessionIntegration, int) {
|
||
podmanOptions := p.MakeOptions(args, PodmanExecOptions{})
|
||
GinkgoWriter.Printf("Running: %s %s\n", p.PodmanBinary, strings.Join(podmanOptions, " "))
|
||
|
||
command := exec.Command(p.PodmanBinary, podmanOptions...)
|
||
session, err := Start(command, GinkgoWriter, GinkgoWriter)
|
||
if err != nil {
|
||
Fail("unable to run podman command: " + strings.Join(podmanOptions, " "))
|
||
}
|
||
podmanSession := &PodmanSession{Session: session}
|
||
return &PodmanSessionIntegration{podmanSession}, command.Process.Pid
|
||
}
|
||
|
||
func (p *PodmanTestIntegration) Quadlet(args []string, sourceDir string) *PodmanSessionIntegration {
|
||
GinkgoWriter.Printf("Running: %s %s with QUADLET_UNIT_DIRS=%s\n", p.QuadletBinary, strings.Join(args, " "), sourceDir)
|
||
|
||
// quadlet uses PODMAN env to get a stable podman path
|
||
podmanPath, found := os.LookupEnv("PODMAN")
|
||
if !found {
|
||
podmanPath = p.PodmanBinary
|
||
}
|
||
|
||
command := exec.Command(p.QuadletBinary, args...)
|
||
command.Env = []string{
|
||
fmt.Sprintf("QUADLET_UNIT_DIRS=%s", sourceDir),
|
||
fmt.Sprintf("PODMAN=%s", podmanPath),
|
||
}
|
||
session, err := Start(command, GinkgoWriter, GinkgoWriter)
|
||
if err != nil {
|
||
Fail("unable to run quadlet command: " + strings.Join(args, " "))
|
||
}
|
||
quadletSession := &PodmanSession{Session: session}
|
||
return &PodmanSessionIntegration{quadletSession}
|
||
}
|
||
|
||
// Cleanup cleans up the temporary store
|
||
func (p *PodmanTestIntegration) Cleanup() {
|
||
// ginkgo v2 still goes into AfterEach() when Skip() was called,
|
||
// some tests call skip before the podman test is initialized.
|
||
if p == nil {
|
||
return
|
||
}
|
||
|
||
// first stop everything, rm -fa is unreliable
|
||
// https://github.com/containers/podman/issues/18180
|
||
stop := p.Podman([]string{"stop", "--all", "-t", "0"})
|
||
stop.WaitWithDefaultTimeout()
|
||
|
||
// Remove all pods...
|
||
podrm := p.Podman([]string{"pod", "rm", "-fa", "-t", "0"})
|
||
podrm.WaitWithDefaultTimeout()
|
||
|
||
// ...and containers
|
||
rmall := p.Podman([]string{"rm", "-fa", "-t", "0"})
|
||
rmall.WaitWithDefaultTimeout()
|
||
|
||
p.StopRemoteService()
|
||
// Nuke tempdir
|
||
rmAll(p.PodmanBinary, p.TempDir)
|
||
|
||
// Clean up the registries configuration file ENV variable set in Create
|
||
resetRegistriesConfigEnv()
|
||
|
||
// Make sure to only check exit codes after all cleanup is done.
|
||
// An error would cause it to stop and return early otherwise.
|
||
Expect(stop).To(Exit(0), "command: %v\nstdout: %s\nstderr: %s", stop.Command.Args, stop.OutputToString(), stop.ErrorToString())
|
||
checkStderrCleanupError(stop, "stop --all -t0 error logged")
|
||
Expect(podrm).To(Exit(0), "command: %v\nstdout: %s\nstderr: %s", podrm.Command.Args, podrm.OutputToString(), podrm.ErrorToString())
|
||
checkStderrCleanupError(podrm, "pod rm -fa -t0 error logged")
|
||
Expect(rmall).To(Exit(0), "command: %v\nstdout: %s\nstderr: %s", rmall.Command.Args, rmall.OutputToString(), rmall.ErrorToString())
|
||
checkStderrCleanupError(rmall, "rm -fa -t0 error logged")
|
||
}
|
||
|
||
func checkStderrCleanupError(s *PodmanSessionIntegration, cmd string) {
|
||
if strings.Contains(podmanTest.OCIRuntime, "runc") {
|
||
// we cannot check stderr for runc, way to many errors
|
||
return
|
||
}
|
||
// offset is 1 so the stacj trace doesn't link to this helper function here
|
||
ExpectWithOffset(1, s.ErrorToString()).To(BeEmpty(), cmd)
|
||
}
|
||
|
||
// CleanupVolume cleans up the volumes and containers.
|
||
// This already calls Cleanup() internally.
|
||
func (p *PodmanTestIntegration) CleanupVolume() {
|
||
// Remove all containers
|
||
session := p.Podman([]string{"volume", "rm", "-fa"})
|
||
session.WaitWithDefaultTimeout()
|
||
Expect(session).To(Exit(0), "command: %v\nstdout: %s\nstderr: %s", session.Command.Args, session.OutputToString(), session.ErrorToString())
|
||
checkStderrCleanupError(session, "volume rm -fa error logged")
|
||
}
|
||
|
||
// CleanupSecret cleans up the secrets and containers.
|
||
// This already calls Cleanup() internally.
|
||
func (p *PodmanTestIntegration) CleanupSecrets() {
|
||
// Remove all containers
|
||
session := p.Podman([]string{"secret", "rm", "-a"})
|
||
session.Wait(90)
|
||
Expect(session).To(Exit(0), "command: %v\nstdout: %s\nstderr: %s", session.Command.Args, session.OutputToString(), session.ErrorToString())
|
||
checkStderrCleanupError(session, "secret rm -a error logged")
|
||
}
|
||
|
||
// InspectContainerToJSON takes the session output of an inspect
|
||
// container and returns json
|
||
func (s *PodmanSessionIntegration) InspectContainerToJSON() []define.InspectContainerData {
|
||
var i []define.InspectContainerData
|
||
err := jsoniter.Unmarshal(s.Out.Contents(), &i)
|
||
Expect(err).ToNot(HaveOccurred())
|
||
return i
|
||
}
|
||
|
||
// InspectPodToJSON takes the sessions output from a pod inspect and returns json
|
||
func (s *PodmanSessionIntegration) InspectPodToJSON() define.InspectPodData {
|
||
var i []define.InspectPodData
|
||
err := jsoniter.Unmarshal(s.Out.Contents(), &i)
|
||
Expect(err).ToNot(HaveOccurred())
|
||
Expect(i).To(HaveLen(1))
|
||
return i[0]
|
||
}
|
||
|
||
// InspectPodToJSON takes the sessions output from an inspect and returns json
|
||
func (s *PodmanSessionIntegration) InspectPodArrToJSON() []define.InspectPodData {
|
||
var i []define.InspectPodData
|
||
err := jsoniter.Unmarshal(s.Out.Contents(), &i)
|
||
Expect(err).ToNot(HaveOccurred())
|
||
return i
|
||
}
|
||
|
||
// CreatePod creates a pod with no infra container
|
||
// it optionally takes a pod name
|
||
func (p *PodmanTestIntegration) CreatePod(options map[string][]string) (*PodmanSessionIntegration, int, string) {
|
||
var args = []string{"pod", "create", "--infra=false", "--share", ""}
|
||
for k, values := range options {
|
||
for _, v := range values {
|
||
args = append(args, k+"="+v)
|
||
}
|
||
}
|
||
|
||
session := p.Podman(args)
|
||
session.WaitWithDefaultTimeout()
|
||
return session, session.ExitCode(), session.OutputToString()
|
||
}
|
||
|
||
func (p *PodmanTestIntegration) CreateVolume(options map[string][]string) (*PodmanSessionIntegration, int, string) {
|
||
var args = []string{"volume", "create"}
|
||
for k, values := range options {
|
||
for _, v := range values {
|
||
args = append(args, k+"="+v)
|
||
}
|
||
}
|
||
|
||
session := p.Podman(args)
|
||
session.WaitWithDefaultTimeout()
|
||
return session, session.ExitCode(), session.OutputToString()
|
||
}
|
||
|
||
func (p *PodmanTestIntegration) RunTopContainerInPod(name, pod string) *PodmanSessionIntegration {
|
||
return p.RunTopContainerWithArgs(name, []string{"--pod", pod})
|
||
}
|
||
|
||
func (p *PodmanTestIntegration) RunHealthCheck(cid string) error {
|
||
for i := 0; i < 10; i++ {
|
||
hc := p.Podman([]string{"healthcheck", "run", cid})
|
||
hc.WaitWithDefaultTimeout()
|
||
if hc.ExitCode() == 0 {
|
||
return nil
|
||
}
|
||
// Restart container if it's not running
|
||
ps := p.Podman([]string{"ps", "--no-trunc", "--quiet", "--filter", fmt.Sprintf("id=%s", cid)})
|
||
ps.WaitWithDefaultTimeout()
|
||
if ps.ExitCode() == 0 {
|
||
if !strings.Contains(ps.OutputToString(), cid) {
|
||
GinkgoWriter.Printf("Container %s is not running, restarting", cid)
|
||
restart := p.Podman([]string{"restart", cid})
|
||
restart.WaitWithDefaultTimeout()
|
||
if restart.ExitCode() != 0 {
|
||
return fmt.Errorf("unable to restart %s", cid)
|
||
}
|
||
}
|
||
}
|
||
GinkgoWriter.Printf("Waiting for %s to pass healthcheck\n", cid)
|
||
time.Sleep(1 * time.Second)
|
||
}
|
||
return fmt.Errorf("unable to detect %s as running", cid)
|
||
}
|
||
|
||
func (p *PodmanTestIntegration) CreateSeccompJSON(in []byte) (string, error) {
|
||
jsonFile := filepath.Join(p.TempDir, "seccomp.json")
|
||
err := WriteJSONFile(in, jsonFile)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return jsonFile, nil
|
||
}
|
||
|
||
func checkReason(reason string) {
|
||
if len(reason) < 5 {
|
||
panic("Test must specify a reason to skip")
|
||
}
|
||
}
|
||
|
||
func SkipIfRunc(p *PodmanTestIntegration, reason string) {
|
||
checkReason(reason)
|
||
if p.OCIRuntime == "runc" {
|
||
Skip("[runc]: " + reason)
|
||
}
|
||
}
|
||
|
||
func SkipIfRootlessCgroupsV1(reason string) {
|
||
checkReason(reason)
|
||
if isRootless() && !CGROUPSV2 {
|
||
Skip("[rootless]: " + reason)
|
||
}
|
||
}
|
||
|
||
func SkipIfRootless(reason string) {
|
||
checkReason(reason)
|
||
if isRootless() {
|
||
Skip("[rootless]: " + reason)
|
||
}
|
||
}
|
||
|
||
func SkipIfNotRootless(reason string) {
|
||
checkReason(reason)
|
||
if !isRootless() {
|
||
Skip("[notRootless]: " + reason)
|
||
}
|
||
}
|
||
|
||
func SkipIfSystemdNotRunning(reason string) {
|
||
checkReason(reason)
|
||
|
||
cmd := exec.Command("systemctl", "list-units")
|
||
err := cmd.Run()
|
||
if err != nil {
|
||
if _, ok := err.(*exec.Error); ok {
|
||
Skip("[notSystemd]: not running " + reason)
|
||
}
|
||
Expect(err).ToNot(HaveOccurred())
|
||
}
|
||
}
|
||
|
||
func SkipIfNotSystemd(manager, reason string) {
|
||
checkReason(reason)
|
||
if manager != "systemd" {
|
||
Skip("[notSystemd]: " + reason)
|
||
}
|
||
}
|
||
|
||
func SkipOnOSVersion(os, version string, reason string) {
|
||
info := GetHostDistributionInfo()
|
||
if info.Distribution == os && info.Version == version {
|
||
Skip(fmt.Sprintf("[%s %s]: %s", os, version, reason))
|
||
}
|
||
}
|
||
|
||
func SkipIfNotFedora(reason string) {
|
||
info := GetHostDistributionInfo()
|
||
if info.Distribution != "fedora" {
|
||
Skip(reason)
|
||
}
|
||
}
|
||
|
||
type journaldTests struct {
|
||
journaldSkip bool
|
||
journaldOnce sync.Once
|
||
}
|
||
|
||
var journald journaldTests
|
||
|
||
// Check if journalctl is unavailable
|
||
func checkAvailableJournald() {
|
||
f := func() {
|
||
journald.journaldSkip = false
|
||
|
||
cmd := exec.Command("journalctl", "-n", "1")
|
||
if err := cmd.Run(); err != nil {
|
||
journald.journaldSkip = true
|
||
}
|
||
}
|
||
journald.journaldOnce.Do(f)
|
||
}
|
||
|
||
func SkipIfJournaldUnavailable() {
|
||
checkAvailableJournald()
|
||
|
||
// In container, journalctl does not return an error even if
|
||
// journald is unavailable
|
||
SkipIfInContainer("[journald]: journalctl inside a container doesn't work correctly")
|
||
if journald.journaldSkip {
|
||
Skip("[journald]: journald is unavailable")
|
||
}
|
||
}
|
||
|
||
// Use isRootless() instead of rootless.IsRootless()
|
||
// This function can detect to join the user namespace by mistake
|
||
func isRootless() bool {
|
||
return os.Geteuid() != 0
|
||
}
|
||
|
||
func isCgroupsV1() bool {
|
||
return !CGROUPSV2
|
||
}
|
||
|
||
func SkipIfCgroupV1(reason string) {
|
||
checkReason(reason)
|
||
if isCgroupsV1() {
|
||
Skip(reason)
|
||
}
|
||
}
|
||
|
||
func SkipIfCgroupV2(reason string) {
|
||
checkReason(reason)
|
||
if CGROUPSV2 {
|
||
Skip(reason)
|
||
}
|
||
}
|
||
|
||
func isContainerized() bool {
|
||
// This is set to "podman" by podman automatically
|
||
return os.Getenv("container") != ""
|
||
}
|
||
|
||
func SkipIfContainerized(reason string) {
|
||
checkReason(reason)
|
||
if isContainerized() {
|
||
Skip(reason)
|
||
}
|
||
}
|
||
|
||
func SkipIfRemote(reason string) {
|
||
checkReason(reason)
|
||
if !IsRemote() {
|
||
return
|
||
}
|
||
Skip("[remote]: " + reason)
|
||
}
|
||
|
||
func SkipIfNotRemote(reason string) {
|
||
checkReason(reason)
|
||
if IsRemote() {
|
||
return
|
||
}
|
||
Skip("[local]: " + reason)
|
||
}
|
||
|
||
// SkipIfInContainer skips a test if the test is run inside a container
|
||
func SkipIfInContainer(reason string) {
|
||
checkReason(reason)
|
||
if os.Getenv("TEST_ENVIRON") == "container" {
|
||
Skip("[container]: " + reason)
|
||
}
|
||
}
|
||
|
||
// SkipIfNotActive skips a test if the given systemd unit is not active
|
||
func SkipIfNotActive(unit string, reason string) {
|
||
checkReason(reason)
|
||
|
||
cmd := exec.Command("systemctl", "is-active", unit)
|
||
cmd.Stdout = GinkgoWriter
|
||
cmd.Stderr = GinkgoWriter
|
||
err := cmd.Run()
|
||
if cmd.ProcessState.ExitCode() == 0 {
|
||
return
|
||
}
|
||
Skip(fmt.Sprintf("[systemd]: unit %s is not active (%v): %s", unit, err, reason))
|
||
}
|
||
|
||
func SkipIfCNI(p *PodmanTestIntegration) {
|
||
if p.NetworkBackend == CNI {
|
||
Skip("this test is not compatible with the CNI network backend")
|
||
}
|
||
}
|
||
|
||
func SkipIfNetavark(p *PodmanTestIntegration) {
|
||
if p.NetworkBackend == Netavark {
|
||
Skip("This test is not compatible with the netavark network backend")
|
||
}
|
||
}
|
||
|
||
// PodmanAsUser is the exec call to podman on the filesystem with the specified uid/gid and environment
|
||
func (p *PodmanTestIntegration) PodmanAsUser(args []string, uid, gid uint32, cwd string, env []string) *PodmanSessionIntegration {
|
||
podmanSession := p.PodmanExecBaseWithOptions(args, PodmanExecOptions{
|
||
UID: uid,
|
||
GID: gid,
|
||
CWD: cwd,
|
||
Env: env,
|
||
})
|
||
return &PodmanSessionIntegration{podmanSession}
|
||
}
|
||
|
||
// RestartRemoteService stop and start API Server, usually to change config
|
||
func (p *PodmanTestIntegration) RestartRemoteService() {
|
||
p.StopRemoteService()
|
||
p.StartRemoteService()
|
||
}
|
||
|
||
// RestoreArtifactToCache populates the imagecache from tarballs that were cached earlier
|
||
func (p *PodmanTestIntegration) RestoreArtifactToCache(image string) error {
|
||
tarball := imageTarPath(image)
|
||
if _, err := os.Stat(tarball); err == nil {
|
||
GinkgoWriter.Printf("Restoring %s...\n", image)
|
||
p.Root = p.ImageCacheDir
|
||
restore := p.PodmanNoEvents([]string{"load", "-q", "-i", tarball})
|
||
restore.WaitWithDefaultTimeout()
|
||
Expect(restore).To(ExitCleanly())
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func populateCache(podman *PodmanTestIntegration) {
|
||
for _, image := range CACHE_IMAGES {
|
||
// FIXME: Remove this hack once composefs can be used with images
|
||
// pulled from sources other than a registry.
|
||
if strings.Contains(podman.StorageOptions, "overlay.use_composefs=true") {
|
||
podman.pullImage(image, true)
|
||
} else {
|
||
err := podman.RestoreArtifactToCache(image)
|
||
Expect(err).ToNot(HaveOccurred())
|
||
}
|
||
}
|
||
// logformatter uses this to recognize the first test
|
||
GinkgoWriter.Printf("-----------------------------\n")
|
||
}
|
||
|
||
// rmAll removes the directory and its content, when running rootless we use
|
||
// podman unshare to prevent any subuid/gid problems
|
||
func rmAll(podmanBin string, path string) {
|
||
// Remove cache dirs
|
||
if isRootless() {
|
||
// If rootless, os.RemoveAll() is failed due to permission denied
|
||
cmd := exec.Command(podmanBin, "unshare", "rm", "-rf", path)
|
||
cmd.Stdout = GinkgoWriter
|
||
cmd.Stderr = GinkgoWriter
|
||
if err := cmd.Run(); err != nil {
|
||
GinkgoWriter.Printf("%v\n", err)
|
||
}
|
||
} else {
|
||
if err = os.RemoveAll(path); err != nil {
|
||
GinkgoWriter.Printf("%q\n", err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// PodmanNoCache calls the podman command with no configured imagecache
|
||
func (p *PodmanTestIntegration) PodmanNoCache(args []string) *PodmanSessionIntegration {
|
||
podmanSession := p.PodmanBase(args, false, true)
|
||
return &PodmanSessionIntegration{podmanSession}
|
||
}
|
||
|
||
func PodmanTestSetup(tempDir string) *PodmanTestIntegration {
|
||
return PodmanTestCreateUtil(tempDir, false)
|
||
}
|
||
|
||
// PodmanNoEvents calls the Podman command without an imagecache and without an
|
||
// events backend. It is used mostly for caching and uncaching images.
|
||
func (p *PodmanTestIntegration) PodmanNoEvents(args []string) *PodmanSessionIntegration {
|
||
podmanSession := p.PodmanBase(args, true, true)
|
||
return &PodmanSessionIntegration{podmanSession}
|
||
}
|
||
|
||
// MakeOptions assembles all the podman main options
|
||
func (p *PodmanTestIntegration) makeOptions(args []string, options PodmanExecOptions) []string {
|
||
if p.RemoteTest {
|
||
if !slices.Contains(args, "--remote") {
|
||
return append([]string{"--remote", "--url", p.RemoteSocket}, args...)
|
||
}
|
||
return args
|
||
}
|
||
|
||
var debug string
|
||
if _, ok := os.LookupEnv("E2E_DEBUG"); ok {
|
||
debug = "--log-level=debug --syslog=true "
|
||
}
|
||
|
||
eventsType := "file"
|
||
if options.NoEvents {
|
||
eventsType = "none"
|
||
}
|
||
|
||
podmanOptions := strings.Split(fmt.Sprintf("%s--root %s --runroot %s --runtime %s --conmon %s --network-config-dir %s --network-backend %s --cgroup-manager %s --tmpdir %s --events-backend %s --db-backend %s",
|
||
debug, p.Root, p.RunRoot, p.OCIRuntime, p.ConmonBinary, p.NetworkConfigDir, p.NetworkBackend.ToString(), p.CgroupManager, p.TmpDir, eventsType, p.DatabaseBackend), " ")
|
||
|
||
podmanOptions = append(podmanOptions, strings.Split(p.StorageOptions, " ")...)
|
||
if !options.NoCache {
|
||
cacheOptions := []string{"--storage-opt",
|
||
fmt.Sprintf("%s.imagestore=%s", p.PodmanTest.ImageCacheFS, p.PodmanTest.ImageCacheDir)}
|
||
podmanOptions = append(cacheOptions, podmanOptions...)
|
||
}
|
||
podmanOptions = append(podmanOptions, args...)
|
||
return podmanOptions
|
||
}
|
||
|
||
func writeConf(conf []byte, confPath string) {
|
||
if _, err := os.Stat(filepath.Dir(confPath)); os.IsNotExist(err) {
|
||
if err := os.MkdirAll(filepath.Dir(confPath), 0o777); err != nil {
|
||
GinkgoWriter.Println(err)
|
||
}
|
||
}
|
||
if err := os.WriteFile(confPath, conf, 0o777); err != nil {
|
||
GinkgoWriter.Println(err)
|
||
}
|
||
}
|
||
|
||
func removeConf(confPath string) {
|
||
if err := os.Remove(confPath); err != nil {
|
||
GinkgoWriter.Println(err)
|
||
}
|
||
}
|
||
|
||
// generateNetworkConfig generates a CNI or Netavark config with a random name
|
||
// it returns the network name and the filepath
|
||
func generateNetworkConfig(p *PodmanTestIntegration) (string, string) {
|
||
var (
|
||
path string
|
||
conf string
|
||
)
|
||
// generate a random name to prevent conflicts with other tests
|
||
name := "net" + stringid.GenerateRandomID()
|
||
if p.NetworkBackend != Netavark {
|
||
path = filepath.Join(p.NetworkConfigDir, fmt.Sprintf("%s.conflist", name))
|
||
conf = fmt.Sprintf(`{
|
||
"cniVersion": "0.3.0",
|
||
"name": "%s",
|
||
"plugins": [
|
||
{
|
||
"type": "bridge",
|
||
"bridge": "cni1",
|
||
"isGateway": true,
|
||
"ipMasq": true,
|
||
"ipam": {
|
||
"type": "host-local",
|
||
"subnet": "10.99.0.0/16",
|
||
"routes": [
|
||
{ "dst": "0.0.0.0/0" }
|
||
]
|
||
}
|
||
},
|
||
{
|
||
"type": "portmap",
|
||
"capabilities": {
|
||
"portMappings": true
|
||
}
|
||
}
|
||
]
|
||
}`, name)
|
||
} else {
|
||
path = filepath.Join(p.NetworkConfigDir, fmt.Sprintf("%s.json", name))
|
||
conf = fmt.Sprintf(`
|
||
{
|
||
"name": "%s",
|
||
"id": "e1ef2749024b88f5663ca693a9118e036d6bfc48bcfe460faf45e9614a513e5c",
|
||
"driver": "bridge",
|
||
"network_interface": "netavark1",
|
||
"created": "2022-01-05T14:15:10.975493521-06:00",
|
||
"subnets": [
|
||
{
|
||
"subnet": "10.100.0.0/16",
|
||
"gateway": "10.100.0.1"
|
||
}
|
||
],
|
||
"ipv6_enabled": false,
|
||
"internal": false,
|
||
"dns_enabled": true,
|
||
"ipam_options": {
|
||
"driver": "host-local"
|
||
}
|
||
}
|
||
`, name)
|
||
}
|
||
writeConf([]byte(conf), path)
|
||
return name, path
|
||
}
|
||
|
||
func (p *PodmanTestIntegration) removeNetwork(name string) {
|
||
session := p.Podman([]string{"network", "rm", "-f", name})
|
||
session.WaitWithDefaultTimeout()
|
||
Expect(session.ExitCode()).To(BeNumerically("<=", 1), "Exit code must be 0 or 1")
|
||
}
|
||
|
||
// generatePolicyFile generates a signature verification policy file.
|
||
// it returns the policy file path.
|
||
func generatePolicyFile(tempDir string, port int) string {
|
||
keyPath := filepath.Join(tempDir, "key.gpg")
|
||
policyPath := filepath.Join(tempDir, "policy.json")
|
||
conf := fmt.Sprintf(`
|
||
{
|
||
"default": [
|
||
{
|
||
"type": "insecureAcceptAnything"
|
||
}
|
||
],
|
||
"transports": {
|
||
"docker": {
|
||
"localhost:%[1]d": [
|
||
{
|
||
"type": "signedBy",
|
||
"keyType": "GPGKeys",
|
||
"keyPath": "%[2]s"
|
||
}
|
||
],
|
||
"localhost:%[1]d/sigstore-signed": [
|
||
{
|
||
"type": "sigstoreSigned",
|
||
"keyPath": "testdata/sigstore-key.pub"
|
||
}
|
||
],
|
||
"localhost:%[1]d/sigstore-signed-params": [
|
||
{
|
||
"type": "sigstoreSigned",
|
||
"keyPath": "testdata/sigstore-key.pub"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
`, port, keyPath)
|
||
writeConf([]byte(conf), policyPath)
|
||
return policyPath
|
||
}
|
||
|
||
func (s *PodmanSessionIntegration) jq(jqCommand string) (string, error) {
|
||
var out bytes.Buffer
|
||
cmd := exec.Command("jq", jqCommand)
|
||
cmd.Stdin = strings.NewReader(s.OutputToString())
|
||
cmd.Stdout = &out
|
||
err := cmd.Run()
|
||
return strings.TrimRight(out.String(), "\n"), err
|
||
}
|
||
|
||
func (p *PodmanTestIntegration) buildImage(dockerfile, imageName string, layers string, label string, extraOptions []string) string {
|
||
dockerfilePath := filepath.Join(p.TempDir, "Dockerfile-"+stringid.GenerateRandomID())
|
||
err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0755)
|
||
Expect(err).ToNot(HaveOccurred())
|
||
cmd := []string{"build", "--pull-never", "--layers=" + layers, "--file", dockerfilePath}
|
||
if label != "" {
|
||
cmd = append(cmd, "--label="+label)
|
||
}
|
||
if len(imageName) > 0 {
|
||
cmd = append(cmd, []string{"-t", imageName}...)
|
||
}
|
||
if len(extraOptions) > 0 {
|
||
cmd = append(cmd, extraOptions...)
|
||
}
|
||
cmd = append(cmd, p.TempDir)
|
||
session := p.Podman(cmd)
|
||
session.Wait(240)
|
||
Expect(session).Should(Exit(0), fmt.Sprintf("BuildImage session output: %q", session.OutputToString()))
|
||
output := session.OutputToStringArray()
|
||
return output[len(output)-1]
|
||
}
|
||
|
||
func writeYaml(content string, fileName string) error {
|
||
f, err := os.Create(fileName)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer f.Close()
|
||
|
||
_, err = f.WriteString(content)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetPort finds an unused TCP/IP port on the system, in the range 5000-5999
|
||
func GetPort() int {
|
||
portMin := 5000
|
||
portMax := 5999
|
||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||
|
||
// Avoid dup-allocation races between parallel ginkgo processes
|
||
nProcs := GinkgoT().ParallelTotal()
|
||
myProc := GinkgoT().ParallelProcess() - 1
|
||
|
||
for i := 0; i < 50; i++ {
|
||
// Random port within that range
|
||
port := portMin + rng.Intn((portMax-portMin)/nProcs)*nProcs + myProc
|
||
|
||
used, err := net.Listen("tcp", "0.0.0.0:"+strconv.Itoa(port))
|
||
if err == nil {
|
||
// it's open. Return it.
|
||
err = used.Close()
|
||
Expect(err).ToNot(HaveOccurred(), "closing random port")
|
||
return port
|
||
}
|
||
}
|
||
|
||
Fail(fmt.Sprintf("unable to get free port in range %d-%d", portMin, portMax))
|
||
return 0 // notreached
|
||
}
|
||
|
||
func ncz(port int) bool {
|
||
timeout := 500 * time.Millisecond
|
||
for i := 0; i < 5; i++ {
|
||
ncCmd := []string{"-z", "localhost", strconv.Itoa(port)}
|
||
GinkgoWriter.Printf("Running: nc %s\n", strings.Join(ncCmd, " "))
|
||
check := SystemExec("nc", ncCmd)
|
||
if check.ExitCode() == 0 {
|
||
return true
|
||
}
|
||
time.Sleep(timeout)
|
||
timeout++
|
||
}
|
||
return false
|
||
}
|
||
|
||
func createNetworkName(name string) string {
|
||
return name + stringid.GenerateRandomID()[:10]
|
||
}
|
||
|
||
var IPRegex = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`
|
||
|
||
// digShort execs into the given container and does a dig lookup with a timeout
|
||
// backoff. If it gets a response, it ensures that the output is in the correct
|
||
// format and iterates a string array for match
|
||
func digShort(container, lookupName, expectedIP string, p *PodmanTestIntegration) {
|
||
digInterval := time.Millisecond * 250
|
||
for i := 0; i < 6; i++ {
|
||
time.Sleep(digInterval * time.Duration(i))
|
||
dig := p.Podman([]string{"exec", container, "dig", "+short", lookupName})
|
||
dig.WaitWithDefaultTimeout()
|
||
output := dig.OutputToString()
|
||
if dig.ExitCode() == 0 && output != "" {
|
||
Expect(output).To(Equal(expectedIP))
|
||
// success
|
||
return
|
||
}
|
||
}
|
||
Fail("dns is not responding")
|
||
}
|
||
|
||
// WaitForFile to be created in defaultWaitTimeout seconds, returns false if file not created
|
||
func WaitForFile(path string) (err error) {
|
||
until := time.Now().Add(time.Duration(defaultWaitTimeout) * time.Second)
|
||
for time.Now().Before(until) {
|
||
_, err = os.Stat(path)
|
||
switch {
|
||
case err == nil:
|
||
return nil
|
||
case errors.Is(err, os.ErrNotExist):
|
||
time.Sleep(10 * time.Millisecond)
|
||
default:
|
||
return err
|
||
}
|
||
}
|
||
return err
|
||
}
|
||
|
||
// WaitForService blocks for defaultWaitTimeout seconds, waiting for some service listening on given host:port
|
||
func WaitForService(address url.URL) {
|
||
// Wait for podman to be ready
|
||
var err error
|
||
until := time.Now().Add(time.Duration(defaultWaitTimeout) * time.Second)
|
||
for time.Now().Before(until) {
|
||
var conn net.Conn
|
||
conn, err = net.Dial("tcp", address.Host)
|
||
if err == nil {
|
||
conn.Close()
|
||
break
|
||
}
|
||
|
||
// Podman not available yet...
|
||
time.Sleep(10 * time.Millisecond)
|
||
}
|
||
Expect(err).ShouldNot(HaveOccurred())
|
||
}
|
||
|
||
// useCustomNetworkDir makes sure this test uses a custom network dir.
|
||
// This needs to be called for all test they may remove networks from other tests,
|
||
// so netwokr prune, system prune, or system reset.
|
||
// see https://github.com/containers/podman/issues/17946
|
||
// Note that when using this and running containers with custom networks you must use the
|
||
// ginkgo Serial decorator to ensure no parallel test are running otherwise we get flakes,
|
||
// https://github.com/containers/podman/issues/23876
|
||
func useCustomNetworkDir(podmanTest *PodmanTestIntegration, tempdir string) {
|
||
// set custom network directory to prevent flakes since the dir is shared with all tests by default
|
||
podmanTest.NetworkConfigDir = tempdir
|
||
if IsRemote() {
|
||
podmanTest.RestartRemoteService()
|
||
}
|
||
}
|
||
|
||
// copy directories recursively from source path to destination path
|
||
func CopyDirectory(srcDir, dest string) error {
|
||
entries, err := os.ReadDir(srcDir)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
for _, entry := range entries {
|
||
sourcePath := filepath.Join(srcDir, entry.Name())
|
||
destPath := filepath.Join(dest, entry.Name())
|
||
|
||
fileInfo, err := os.Stat(sourcePath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
switch fileInfo.Mode() & os.ModeType {
|
||
case os.ModeDir:
|
||
if err := os.MkdirAll(destPath, 0755); err != nil {
|
||
return fmt.Errorf("failed to create directory: %q, error: %q", destPath, err.Error())
|
||
}
|
||
if err := CopyDirectory(sourcePath, destPath); err != nil {
|
||
return err
|
||
}
|
||
case os.ModeSymlink:
|
||
if err := CopySymLink(sourcePath, destPath); err != nil {
|
||
return err
|
||
}
|
||
default:
|
||
if err := Copy(sourcePath, destPath); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
fInfo, err := entry.Info()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
isSymlink := fInfo.Mode()&os.ModeSymlink != 0
|
||
if !isSymlink {
|
||
if err := os.Chmod(destPath, fInfo.Mode()); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func Copy(srcFile, dstFile string) error {
|
||
out, err := os.Create(dstFile)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
defer out.Close()
|
||
|
||
in, err := os.Open(srcFile)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
_, err = io.Copy(out, in)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer in.Close()
|
||
return nil
|
||
}
|
||
|
||
func CopySymLink(source, dest string) error {
|
||
link, err := os.Readlink(source)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return os.Symlink(link, dest)
|
||
}
|
||
|
||
func UsingCacheRegistry() bool {
|
||
return os.Getenv("CI_USE_REGISTRY_CACHE") != ""
|
||
}
|
||
|
||
func setupRegistry(portOverride *int) (*lockfile.LockFile, string, error) {
|
||
var port string
|
||
if isRootless() {
|
||
if err := podmanTest.RestoreArtifact(REGISTRY_IMAGE); err != nil {
|
||
return nil, "", err
|
||
}
|
||
}
|
||
|
||
if portOverride != nil {
|
||
port = strconv.Itoa(*portOverride)
|
||
} else {
|
||
p, err := utils.GetRandomPort()
|
||
if err != nil {
|
||
return nil, "", err
|
||
}
|
||
port = strconv.Itoa(p)
|
||
}
|
||
|
||
lock := GetPortLock(port)
|
||
|
||
session := podmanTest.Podman([]string{"run", "-d", "--name", "registry", "-p", fmt.Sprintf("%s:5000", port), REGISTRY_IMAGE, "/entrypoint.sh", "/etc/docker/registry/config.yml"})
|
||
session.WaitWithDefaultTimeout()
|
||
Expect(session).Should(ExitCleanly())
|
||
|
||
if !WaitContainerReady(podmanTest, "registry", "listening on", 20, 1) {
|
||
lock.Unlock()
|
||
Skip("Cannot start docker registry.")
|
||
}
|
||
return lock, port, nil
|
||
}
|
||
|
||
func createArtifactFile(numBytes int64) (string, error) {
|
||
artifactDir := filepath.Join(podmanTest.TempDir, "artifacts")
|
||
if err := os.MkdirAll(artifactDir, 0755); err != nil {
|
||
return "", err
|
||
}
|
||
filename := RandomString(8)
|
||
outFile := filepath.Join(artifactDir, filename)
|
||
session := podmanTest.Podman([]string{"run", "-v", fmt.Sprintf("%s:/artifacts:z", artifactDir), ALPINE, "dd", "if=/dev/urandom", fmt.Sprintf("of=%s", filepath.Join("/artifacts", filename)), "bs=1b", fmt.Sprintf("count=%d", numBytes)})
|
||
session.WaitWithDefaultTimeout()
|
||
if session.ExitCode() != 0 {
|
||
return "", errors.New("unable to generate artifact file")
|
||
}
|
||
return outFile, nil
|
||
}
|