skopeo/integration/utils_test.go
Kir Kolyshkin 25481e1a88 integration: add _test suffix to files
Some files in integration did not have _test, resulting in lots of
complains when running golangci-lint with --tests=false.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
2025-04-14 12:29:02 +02:00

292 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"bytes"
"compress/gzip"
"encoding/json"
"io"
"net"
"net/netip"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/containers/image/v5/manifest"
"github.com/opencontainers/go-digest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const skopeoBinary = "skopeo"
const testFQIN = "docker://quay.io/libpod/busybox" // tag left off on purpose, some tests need to add a special one
const testFQIN64 = "docker://quay.io/libpod/busybox:amd64"
const testFQINMultiLayer = "docker://quay.io/libpod/alpine_nginx:latest" // multi-layer
// consumeAndLogOutputStream takes (f, err) from an exec.*Pipe(), and causes all output to it to be logged to t.
func consumeAndLogOutputStream(t *testing.T, id string, f io.ReadCloser, err error) {
require.NoError(t, err)
go func() {
defer func() {
f.Close()
t.Logf("Output %s: Closed", id)
}()
buf := make([]byte, 1024)
for {
t.Logf("Output %s: waiting", id)
n, err := f.Read(buf)
t.Logf("Output %s: got %d,%#v: %s", id, n, err, strings.TrimSuffix(string(buf[:n]), "\n"))
if n <= 0 {
break
}
}
}()
}
// consumeAndLogOutputs causes all output to stdout and stderr from an *exec.Cmd to be logged to c.
func consumeAndLogOutputs(t *testing.T, id string, cmd *exec.Cmd) {
stdout, err := cmd.StdoutPipe()
consumeAndLogOutputStream(t, id+" stdout", stdout, err)
stderr, err := cmd.StderrPipe()
consumeAndLogOutputStream(t, id+" stderr", stderr, err)
}
// combinedOutputOfCommand runs a command as if exec.Command().CombinedOutput(), verifies that the exit status is 0, and returns the output,
// or terminates c on failure.
func combinedOutputOfCommand(t *testing.T, name string, args ...string) string {
t.Logf("Running %s %s", name, strings.Join(args, " "))
out, err := exec.Command(name, args...).CombinedOutput()
require.NoError(t, err, "%s", out)
return string(out)
}
// assertSkopeoSucceeds runs a skopeo command as if exec.Command().CombinedOutput, verifies that the exit status is 0,
// and optionally that the output matches a multi-line regexp if it is nonempty;
// or terminates c on failure
func assertSkopeoSucceeds(t *testing.T, regexp string, args ...string) {
t.Logf("Running %s %s", skopeoBinary, strings.Join(args, " "))
out, err := exec.Command(skopeoBinary, args...).CombinedOutput()
assert.NoError(t, err, "%s", out)
if regexp != "" {
assert.Regexp(t, "(?s)"+regexp, string(out)) // (?s) : '.' will also match newlines
}
}
// assertSkopeoFails runs a skopeo command as if exec.Command().CombinedOutput, verifies that the exit status is 0,
// and that the output matches a multi-line regexp;
// or terminates c on failure
func assertSkopeoFails(t *testing.T, regexp string, args ...string) {
t.Logf("Running %s %s", skopeoBinary, strings.Join(args, " "))
out, err := exec.Command(skopeoBinary, args...).CombinedOutput()
assert.Error(t, err, "%s", out)
assert.Regexp(t, "(?s)"+regexp, string(out)) // (?s) : '.' will also match newlines
}
// runCommandWithInput runs a command as if exec.Command(), sending it the input to stdin,
// and verifies that the exit status is 0, or terminates c on failure.
func runCommandWithInput(t *testing.T, input string, name string, args ...string) {
cmd := exec.Command(name, args...)
runExecCmdWithInput(t, cmd, input)
}
// runExecCmdWithInput runs an exec.Cmd, sending it the input to stdin,
// and verifies that the exit status is 0, or terminates c on failure.
func runExecCmdWithInput(t *testing.T, cmd *exec.Cmd, input string) {
t.Logf("Running %s %s", cmd.Path, strings.Join(cmd.Args, " "))
consumeAndLogOutputs(t, cmd.Path+" "+strings.Join(cmd.Args, " "), cmd)
stdin, err := cmd.StdinPipe()
require.NoError(t, err)
err = cmd.Start()
require.NoError(t, err)
_, err = io.WriteString(stdin, input)
require.NoError(t, err)
err = stdin.Close()
require.NoError(t, err)
err = cmd.Wait()
assert.NoError(t, err)
}
// isPortOpen returns true iff the specified port on localhost is open.
func isPortOpen(port uint16) bool {
ap := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), port)
conn, err := net.DialTCP("tcp", nil, net.TCPAddrFromAddrPort(ap))
if err != nil {
return false
}
conn.Close()
return true
}
// newPortChecker sets up a portOpen channel which will receive true after the specified port is open.
// The checking can be aborted by sending a value to the terminate channel, which the caller should
// always do using
// defer func() {terminate <- true}()
func newPortChecker(t *testing.T, port uint16) (portOpen <-chan bool, terminate chan<- bool) {
portOpenBidi := make(chan bool)
// Buffered, so that sending a terminate request after the goroutine has exited does not block.
terminateBidi := make(chan bool, 1)
go func() {
defer func() {
t.Logf("Port checker for port %d exiting", port)
}()
for {
t.Logf("Checking for port %d...", port)
if isPortOpen(port) {
t.Logf("Port %d open", port)
portOpenBidi <- true
return
}
t.Logf("Sleeping for port %d", port)
sleepChan := time.After(100 * time.Millisecond)
select {
case <-sleepChan: // Try again
t.Logf("Sleeping for port %d done, will retry", port)
case <-terminateBidi:
t.Logf("Check for port %d terminated", port)
return
}
}
}()
return portOpenBidi, terminateBidi
}
// modifyEnviron modifies os.Environ()-like list of name=value assignments to set name to value.
func modifyEnviron(env []string, name, value string) []string {
prefix := name + "="
res := []string{}
for _, e := range env {
if !strings.HasPrefix(e, prefix) {
res = append(res, e)
}
}
return append(res, prefix+value)
}
// fileFromFixture applies edits to inputPath and returns a path to the temporary file.
// Callers should defer os.Remove(the_returned_path)
func fileFromFixture(t *testing.T, inputPath string, edits map[string]string) string {
contents, err := os.ReadFile(inputPath)
require.NoError(t, err)
for template, value := range edits {
updated := bytes.ReplaceAll(contents, []byte(template), []byte(value))
require.NotEqual(t, contents, updated, "Replacing %s in %#v failed", template, string(contents)) // Verify that the template has matched something and we are not silently ignoring it.
contents = updated
}
file, err := os.CreateTemp("", "policy.json")
require.NoError(t, err)
path := file.Name()
_, err = file.Write(contents)
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)
return path
}
// decompressDirs decompresses specified dir:-formatted directories
func decompressDirs(t *testing.T, dirs ...string) {
t.Logf("Decompressing %s", strings.Join(dirs, " "))
for i, dir := range dirs {
m, err := os.ReadFile(filepath.Join(dir, "manifest.json"))
require.NoError(t, err)
t.Logf("manifest %d before: %s", i+1, string(m))
decompressDir(t, dir)
m, err = os.ReadFile(filepath.Join(dir, "manifest.json"))
require.NoError(t, err)
t.Logf("manifest %d after: %s", i+1, string(m))
}
}
// getRawMapField assigns a value of rawMap[key] to dest,
// failing if it does not exist or if it doesnt have the expected type
func getRawMapField[T any](t *testing.T, rawMap map[string]any, key string, dest *T) {
rawValue, ok := rawMap[key]
require.True(t, ok, key)
value, ok := rawValue.(T)
require.True(t, ok, key, "%#v", value)
*dest = value
}
// decompressDir modifies a dir:-formatted directory to replace gzip-compressed layers with uncompressed variants,
// and to use a ~canonical formatting of manifest.json.
func decompressDir(t *testing.T, dir string) {
// This is, overall, very dumb; the “obvious” way would be to invoke skopeo to decompress,
// or at least to use c/image to parse/format the manifest.
//
// But this is used to test (aspects of) those code paths… so, its acceptable for this to be
// dumb and to make assumptions about the data, but it should not share code.
manifestBlob, err := os.ReadFile(filepath.Join(dir, "manifest.json"))
require.NoError(t, err)
var rawManifest map[string]any
err = json.Unmarshal(manifestBlob, &rawManifest)
require.NoError(t, err)
var rawLayers []any
getRawMapField(t, rawManifest, "layers", &rawLayers)
for i, rawLayerValue := range rawLayers {
rawLayer, ok := rawLayerValue.(map[string]any)
require.True(t, ok)
var digestString string
getRawMapField(t, rawLayer, "digest", &digestString)
compressedDigest, err := digest.Parse(digestString)
require.NoError(t, err)
if compressedDigest.String() == "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" { // An empty file
continue
}
compressedPath := filepath.Join(dir, compressedDigest.Encoded())
compressedStream, err := os.Open(compressedPath)
require.NoError(t, err)
defer compressedStream.Close()
uncompressedStream, err := gzip.NewReader(compressedStream)
if err != nil {
continue // Silently assume the layer is not gzip-compressed
}
tempDest, err := os.CreateTemp(dir, "decompressing")
require.NoError(t, err)
digester := digest.Canonical.Digester()
uncompressedSize, err := io.Copy(tempDest, io.TeeReader(uncompressedStream, digester.Hash()))
require.NoError(t, err)
err = uncompressedStream.Close()
require.NoError(t, err)
uncompressedDigest := digester.Digest()
uncompressedPath := filepath.Join(dir, uncompressedDigest.Encoded())
err = os.Rename(tempDest.Name(), uncompressedPath)
require.NoError(t, err)
err = os.Remove(compressedPath)
require.NoError(t, err)
rawLayer["digest"] = uncompressedDigest.String()
rawLayer["size"] = uncompressedSize
var mimeType string
getRawMapField(t, rawLayer, "mediaType", &mimeType)
if uncompressedMIMEType, ok := strings.CutSuffix(mimeType, ".gzip"); ok {
rawLayer["mediaType"] = uncompressedMIMEType
}
rawLayers[i] = rawLayer
}
rawManifest["layers"] = rawLayers
manifestBlob, err = json.Marshal(rawManifest)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "manifest.json"), manifestBlob, 0o600)
require.NoError(t, err)
}
// Verify manifest in a dir: image at dir is expectedMIMEType.
func verifyManifestMIMEType(t *testing.T, dir string, expectedMIMEType string) {
manifestBlob, err := os.ReadFile(filepath.Join(dir, "manifest.json"))
require.NoError(t, err)
mimeType := manifest.GuessMIMEType(manifestBlob)
assert.Equal(t, expectedMIMEType, mimeType)
}