test: move test utils files to the test-utils folder

Tests that are currently on the main branch only runs against a qemu VM. We have other use cases that needs to be tested like running against a vfkit VM.
This commit reorganizes the tests code a bit by moving the files that can be shared to support different implementation in their own folder.
The reasoning behind this is that every hypervisor should have its own beforeSuite func to download/run a specific VM image. By moving the utils files we can reuse the same code.

For the same reason the code targeting qemu is moved to the test-qemu folder. By doing so, we can run the tests within the test-qemu folder on the ubuntu workflow and, in future, when the nested virt will be enabled on github runners, the vfkit tests on macOS.

Signed-off-by: Luca Stocchi <lstocchi@redhat.com>
This commit is contained in:
Luca Stocchi
2024-11-19 17:02:57 +01:00
parent 5f09250426
commit b525dc8c69
13 changed files with 161 additions and 108 deletions

View File

@@ -57,7 +57,7 @@ jobs:
- name: Test
run: |
sudo -s -u ${USER} bash -c 'make test'
sudo -s -u ${USER} bash -c 'make test-linux'
- uses: actions/upload-artifact@v4
if: always()

View File

@@ -67,6 +67,6 @@ cross: $(TOOLS_BINDIR)/makefat
test-companion:
GOOS=linux go build -ldflags "$(LDFLAGS)" -o bin/test-companion ./cmd/test-companion
.PHONY: test
test: gvproxy test-companion
go test -timeout 20m -v ./...
.PHONY: test-linux
test-linux: gvproxy test-companion
go test -timeout 20m -v ./test-qemu

45
test-qemu/basic_test.go Normal file
View File

@@ -0,0 +1,45 @@
package e2eqemu
import (
"github.com/containers/gvisor-tap-vsock/pkg/types"
e2e "github.com/containers/gvisor-tap-vsock/test"
"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
)
var _ = ginkgo.Describe("connectivity with qemu", func() {
e2e.BasicConnectivityTests(e2e.BasicTestProps{
SSHExec: sshExec,
})
})
var _ = ginkgo.Describe("dns with qemu", func() {
e2e.BasicDNSTests(e2e.BasicTestProps{
SSHExec: sshExec,
Sock: sock,
})
})
var _ = ginkgo.Describe("command-line format", func() {
ginkgo.It("should convert Command to command line format", func() {
command := types.NewGvproxyCommand()
command.AddEndpoint("unix:///tmp/network.sock")
command.Debug = true
command.AddQemuSocket("tcp://0.0.0.0:1234")
command.PidFile = "~/gv-pidfile.txt"
command.LogFile = "~/gv.log"
command.AddForwardUser("demouser")
cmd := command.ToCmdline()
gomega.Expect(cmd).To(gomega.Equal([]string{
"-listen", "unix:///tmp/network.sock",
"-debug",
"-mtu", "1500",
"-ssh-port", "2222",
"-listen-qemu", "tcp://0.0.0.0:1234",
"-forward-user", "demouser",
"-pid-file", "~/gv-pidfile.txt",
"-log-file", "~/gv.log",
}))
})
})

View File

@@ -1,4 +1,4 @@
package e2e
package e2eqemu
import (
"fmt"

View File

@@ -1,6 +1,6 @@
//go:build !(darwin && arm64)
package e2e
package e2eqemu
func efiArgs() (string, error) {
return "", nil

View File

@@ -1,4 +1,4 @@
package e2e
package e2eqemu
import (
"context"

View File

@@ -1,4 +1,4 @@
package e2e
package e2eqemu
import (
"flag"
@@ -13,9 +13,10 @@ import (
"testing"
"time"
e2e_utils "github.com/containers/gvisor-tap-vsock/test-utils"
"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
@@ -63,15 +64,15 @@ func init() {
var _ = ginkgo.BeforeSuite(func() {
gomega.Expect(os.MkdirAll(filepath.Join(tmpDir, "disks"), os.ModePerm)).Should(gomega.Succeed())
downloader, err := NewFcosDownloader(filepath.Join(tmpDir, "disks"))
downloader, err := e2e_utils.NewFcosDownloader(filepath.Join(tmpDir, "disks"))
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
qemuImage, err := downloader.DownloadImage()
qemuImage, err := downloader.DownloadImage("qemu", "qcow2.xz")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
publicKey, err := createSSHKeys()
publicKey, err := e2e_utils.CreateSSHKeys(publicKeyFile, privateKeyFile)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
err = CreateIgnition(ignFile, publicKey, ignitionUser, ignitionPasswordHash)
err = e2e_utils.CreateIgnition(ignFile, publicKey, ignitionUser, ignitionPasswordHash)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
outer:
@@ -154,7 +155,7 @@ outer:
})
func qemuExecutable() string {
qemuBinaries := []string{"qemu-kvm", fmt.Sprintf("qemu-system-%s", coreosArch())}
qemuBinaries := []string{"qemu-kvm", fmt.Sprintf("qemu-system-%s", e2e_utils.CoreosArch())}
for _, binary := range qemuBinaries {
path, err := exec.LookPath(binary)
if err == nil && path != "" {
@@ -182,26 +183,6 @@ func qemuArgs() string {
return fmt.Sprintf("-machine %s,accel=%s:tcg -smp 4 -cpu host ", machine, accel)
}
func createSSHKeys() (string, error) {
_ = os.Remove(publicKeyFile)
_ = os.Remove(privateKeyFile)
err := exec.Command("ssh-keygen", "-N", "", "-t", "ed25519", "-f", privateKeyFile).Run()
if err != nil {
return "", errors.Wrap(err, "Could not generate ssh keys")
}
return readPublicKey()
}
func readPublicKey() (string, error) {
publicKey, err := os.ReadFile(publicKeyFile)
if err != nil {
return "", nil
}
return strings.TrimSpace(string(publicKey)), nil
}
func scp(src, dst string) error {
sshCmd := exec.Command("scp",
"-o", "UserKnownHostsFile=/dev/null",

View File

@@ -1,4 +1,4 @@
package e2e
package e2eutils
import (
"os"
@@ -27,6 +27,11 @@ type fcosDownloadInfo struct {
Sha256Sum string
}
type ArtifactFormat struct {
Artifact string
Format string
}
func NewFcosDownloader(dataDir string) (*FcosDownload, error) {
return &FcosDownload{
DataDir: dataDir,
@@ -38,14 +43,13 @@ func imageName(info *fcosDownloadInfo) string {
return urlSplit[len(urlSplit)-1]
}
func (downloader *FcosDownload) DownloadImage() (string, error) {
info, err := getFCOSDownload()
func (downloader *FcosDownload) DownloadImage(artifactType string, formatType string) (string, error) {
info, err := getFCOSDownload(artifactType, formatType)
if err != nil {
return "", err
}
compressedImage := filepath.Join(downloader.DataDir, imageName(info))
uncompressedImage := strings.TrimSuffix(filepath.Join(filepath.Dir(compressedImage), imageName(info)), ".xz")
// check if the latest image is already present
ok, err := downloader.updateAvailable(info, compressedImage)
@@ -58,10 +62,8 @@ func (downloader *FcosDownload) DownloadImage() (string, error) {
}
}
if _, err := os.Stat(uncompressedImage); err == nil {
return uncompressedImage, nil
}
if err := Decompress(compressedImage, uncompressedImage); err != nil {
uncompressedImage := ""
if uncompressedImage, err = Decompress(compressedImage); err != nil {
return "", err
}
return uncompressedImage, nil
@@ -91,7 +93,7 @@ func (downloader *FcosDownload) updateAvailable(info *fcosDownloadInfo, compress
// as of 2024-05-28, these are the 4 architectures available in
// curl https://builds.coreos.fedoraproject.org/streams/next.json
func coreosArch() string {
func CoreosArch() string {
switch runtime.GOARCH {
case "amd64":
return "x86_64"
@@ -107,7 +109,7 @@ func coreosArch() string {
// This should get Exported and stay put as it will apply to all fcos downloads
// getFCOS parses fedoraCoreOS's stream and returns the image download URL and the release version
func getFCOSDownload() (*fcosDownloadInfo, error) {
func getFCOSDownload(artifactType string, formatType string) (*fcosDownloadInfo, error) {
streamurl := fedoracoreos.GetStreamURL(fedoracoreos.StreamNext)
resp, err := http.Get(streamurl.String())
if err != nil {
@@ -127,7 +129,7 @@ func getFCOSDownload() (*fcosDownloadInfo, error) {
if err := json.Unmarshal(body, &fcosstable); err != nil {
return nil, err
}
arch, ok := fcosstable.Architectures[coreosArch()]
arch, ok := fcosstable.Architectures[CoreosArch()]
if !ok {
return nil, fmt.Errorf("unable to pull VM image: no targetArch in stream")
}
@@ -135,19 +137,19 @@ func getFCOSDownload() (*fcosDownloadInfo, error) {
if artifacts == nil {
return nil, fmt.Errorf("unable to pull VM image: no artifact in stream")
}
qemu, ok := artifacts["qemu"]
artifact, ok := artifacts[artifactType]
if !ok {
return nil, fmt.Errorf("unable to pull VM image: no qemu artifact in stream")
}
formats := qemu.Formats
formats := artifact.Formats
if formats == nil {
return nil, fmt.Errorf("unable to pull VM image: no formats in stream")
}
qcow, ok := formats["qcow2.xz"]
format, ok := formats[formatType]
if !ok {
return nil, fmt.Errorf("unable to pull VM image: no qcow2.xz format in stream")
}
disk := qcow.Disk
disk := format.Disk
if disk == nil {
return nil, fmt.Errorf("unable to pull VM image: no disk in stream")
}

View File

@@ -1,4 +1,4 @@
package e2e
package e2eutils
import (
"encoding/json"

View File

@@ -1,4 +1,4 @@
package e2e
package e2eutils
// Taken from https://github.com/coreos/ignition/blob/master/config/v3_2/types/schema.go

View File

@@ -1,4 +1,4 @@
package e2e
package e2eutils
import (
"fmt"
@@ -48,18 +48,32 @@ func DownloadVMImage(downloadURL string, localImagePath string) error {
return nil
}
func Decompress(localPath, uncompressedPath string) error {
uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return err
func Decompress(localPath string) (string, error) {
uncompressedPath := ""
if strings.HasSuffix(localPath, ".xz") {
uncompressedPath = strings.TrimSuffix(localPath, ".xz")
}
if !strings.HasSuffix(localPath, ".xz") {
return fmt.Errorf("unsupported compression for %s", localPath)
if uncompressedPath == "" {
return "", fmt.Errorf("unsupported compression for %s", localPath)
}
// we remove the uncompressed file if already exists. Maybe it has been used earlier and can affect the tests result
os.Remove(uncompressedPath)
uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return "", err
}
fmt.Printf("Extracting %s\n", localPath)
return decompressXZ(localPath, uncompressedFileWriter)
err = decompressXZ(localPath, uncompressedFileWriter)
if err != nil {
return "", err
}
return uncompressedPath, nil
}
// Will error out if file without .xz already exists

29
test-utils/ssh.go Normal file
View File

@@ -0,0 +1,29 @@
package e2eutils
import (
"os"
"os/exec"
"strings"
"github.com/pkg/errors"
)
func CreateSSHKeys(publicKeyFile, privateKeyFile string) (string, error) {
_ = os.Remove(publicKeyFile)
_ = os.Remove(privateKeyFile)
err := exec.Command("ssh-keygen", "-N", "", "-t", "ed25519", "-f", privateKeyFile).Run()
if err != nil {
return "", errors.Wrap(err, "Could not generate ssh keys")
}
return readPublicKey(publicKeyFile)
}
func readPublicKey(publicKeyFile string) (string, error) {
publicKey, err := os.ReadFile(publicKeyFile)
if err != nil {
return "", nil
}
return strings.TrimSpace(string(publicKey)), nil
}

View File

@@ -6,14 +6,20 @@ import (
"net/http"
gvproxyclient "github.com/containers/gvisor-tap-vsock/pkg/client"
"github.com/containers/gvisor-tap-vsock/pkg/types"
"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
)
var _ = ginkgo.Describe("connectivity", func() {
type BasicTestProps struct {
SSHExec func(cmd ...string) ([]byte, error)
Sock string
}
func BasicConnectivityTests(props BasicTestProps) {
ginkgo.It("should configure the interface", func() {
out, err := sshExec("ifconfig $(route | grep '^default' | grep -o '[^ ]*$')")
out, err := props.SSHExec("ifconfig $(route | grep '^default' | grep -o '[^ ]*$')")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("mtu 1500"))
gomega.Expect(string(out)).To(gomega.ContainSubstring("inet 192.168.127.2"))
@@ -22,72 +28,72 @@ var _ = ginkgo.Describe("connectivity", func() {
})
ginkgo.It("should configure the default route", func() {
out, err := sshExec("ip route show")
out, err := props.SSHExec("ip route show")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.MatchRegexp(`default via 192\.168\.127\.1 dev (.*?) proto dhcp (src 192\.168\.127\.2 )?metric 100`))
})
ginkgo.It("should configure dns settings", func() {
out, err := sshExec("cat /etc/resolv.conf")
out, err := props.SSHExec("cat /etc/resolv.conf")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("nameserver 192.168.127.1"))
})
ginkgo.It("should ping the tap device", func() {
out, err := sshExec("ping -c2 192.168.127.2")
out, err := props.SSHExec("ping -c2 192.168.127.2")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("2 packets transmitted, 2 received, 0% packet loss"))
})
ginkgo.It("should ping the gateway", func() {
out, err := sshExec("ping -c2 192.168.127.1")
out, err := props.SSHExec("ping -c2 192.168.127.1")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("2 packets transmitted, 2 received, 0% packet loss"))
})
})
}
var _ = ginkgo.Describe("dns", func() {
func BasicDNSTests(props BasicTestProps) {
ginkgo.It("should resolve redhat.com", func() {
out, err := sshExec("nslookup redhat.com")
out, err := props.SSHExec("nslookup redhat.com")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 52.200.142.250"))
})
ginkgo.It("should resolve CNAME record for docs.crc.dev", func() {
out, err := sshExec("nslookup -query=cname docs.crc.dev")
out, err := props.SSHExec("nslookup -query=cname docs.crc.dev")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("docs.crc.dev canonical name = webredir.gandi.net."))
})
ginkgo.It("should resolve MX record for crc.dev", func() {
out, err := sshExec("nslookup -query=mx crc.dev")
out, err := props.SSHExec("nslookup -query=mx crc.dev")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("crc.dev mail exchanger = 10 spool.mail.gandi.net."))
})
ginkgo.It("should resolve NS record for wikipedia.org", func() {
out, err := sshExec("nslookup -query=ns wikipedia.org")
out, err := props.SSHExec("nslookup -query=ns wikipedia.org")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("wikipedia.org nameserver = ns0.wikimedia.org."))
})
ginkgo.It("should resolve IMAPS SRV record for crc.dev", func() {
out, err := sshExec("nslookup -query=srv _imaps._tcp.crc.dev")
out, err := props.SSHExec("nslookup -query=srv _imaps._tcp.crc.dev")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring(`_imaps._tcp.crc.dev service = 0 1 993 mail.gandi.net.`))
})
ginkgo.It("should resolve TXT for crc.dev", func() {
out, err := sshExec("nslookup -query=txt crc.dev")
out, err := props.SSHExec("nslookup -query=txt crc.dev")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring(`text = "v=spf1`))
})
ginkgo.It("should resolve gateway.containers.internal", func() {
out, err := sshExec("nslookup gateway.containers.internal")
out, err := props.SSHExec("nslookup gateway.containers.internal")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.1"))
})
ginkgo.It("should resolve host.containers.internal", func() {
out, err := sshExec("nslookup host.containers.internal")
out, err := props.SSHExec("nslookup host.containers.internal")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.254"))
})
@@ -96,7 +102,7 @@ var _ = ginkgo.Describe("dns", func() {
client := gvproxyclient.New(&http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", sock)
return net.Dial("unix", props.Sock)
},
},
}, "http://base")
@@ -111,7 +117,7 @@ var _ = ginkgo.Describe("dns", func() {
})
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
out, err := sshExec("nslookup test.dynamic.internal")
out, err := props.SSHExec("nslookup test.dynamic.internal")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.254"))
@@ -121,7 +127,7 @@ var _ = ginkgo.Describe("dns", func() {
client := gvproxyclient.New(&http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", sock)
return net.Dial("unix", props.Sock)
},
},
}, "http://base")
@@ -147,7 +153,7 @@ var _ = ginkgo.Describe("dns", func() {
})
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
out, err := sshExec("nslookup test.dynamic.internal")
out, err := props.SSHExec("nslookup test.dynamic.internal")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.253"))
@@ -157,7 +163,7 @@ var _ = ginkgo.Describe("dns", func() {
client := gvproxyclient.New(&http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", sock)
return net.Dial("unix", props.Sock)
},
},
}, "http://base")
@@ -174,7 +180,7 @@ var _ = ginkgo.Describe("dns", func() {
},
},
})
out, err := sshExec("nslookup test.dynamic.internal")
out, err := props.SSHExec("nslookup test.dynamic.internal")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.2"))
@@ -187,36 +193,12 @@ var _ = ginkgo.Describe("dns", func() {
},
},
})
out, err = sshExec("nslookup *.dynamic.testing")
out, err = props.SSHExec("nslookup *.dynamic.testing")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.2"))
out, err = sshExec("nslookup gateway.testing")
out, err = props.SSHExec("nslookup gateway.testing")
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.1"))
})
})
var _ = ginkgo.Describe("command-line format", func() {
ginkgo.It("should convert Command to command line format", func() {
command := types.NewGvproxyCommand()
command.AddEndpoint("unix:///tmp/network.sock")
command.Debug = true
command.AddQemuSocket("tcp://0.0.0.0:1234")
command.PidFile = "~/gv-pidfile.txt"
command.LogFile = "~/gv.log"
command.AddForwardUser("demouser")
cmd := command.ToCmdline()
gomega.Expect(cmd).To(gomega.Equal([]string{
"-listen", "unix:///tmp/network.sock",
"-debug",
"-mtu", "1500",
"-ssh-port", "2222",
"-listen-qemu", "tcp://0.0.0.0:1234",
"-forward-user", "demouser",
"-pid-file", "~/gv-pidfile.txt",
"-log-file", "~/gv.log",
}))
})
})
}